From 0db384f8944ae9d585b0f636c372d06534bfe154 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Nov 2024 16:06:02 +0100 Subject: [PATCH 1/4] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 1847f4d2c4..c2cc0e49e3 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 5dcbbb69fd..2d26e4ffe0 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 38a234cb71..6273bff393 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index c6d9301c02..825f122291 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 3.5.0-GH-3689-SNAPSHOT ../pom.xml From 3d3239c293f1e1f6d66756d7629dd2a69a9cddfb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Nov 2024 16:07:14 +0100 Subject: [PATCH 2/4] Consider CONFLICT clause on INSERT. --- .../data/jpa/repository/query/Hql.g4 | 19 +++++- .../repository/query/HqlQueryRenderer.java | 64 +++++++++++++++++++ .../query/HqlSpecificationTests.java | 33 +++++++--- 3 files changed, 106 insertions(+), 10 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 54a93e9ebf..183a1072d2 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 @@ -147,7 +147,7 @@ deleteStatement // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-insert insertStatement - : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) + : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) conflictClause? ; // Already defined underneath updateStatement @@ -167,6 +167,23 @@ values : '(' expression (',' expression)* ')' ; +/** + * a 'conflict' clause in an 'insert' statement + */ +conflictClause + : ON CONFLICT conflictTarget? DO conflictAction + ; + +conflictTarget + : ON CONSTRAINT identifier + | '(' simplePath (',' simplePath)* ')' + ; + +conflictAction + : NOTHING + | UPDATE setClause whereClause? + ; + instantiation : NEW instantiationTarget '(' instantiationArguments ')' ; 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 9976347f1d..528cdfe263 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 @@ -545,6 +545,10 @@ public QueryTokenStream visitInsertStatement(HqlParser.InsertStatementContext ct builder.appendExpression(visit(ctx.valuesList())); } + if (ctx.conflictClause() != null) { + builder.appendExpression(visit(ctx.conflictClause())); + } + return builder; } @@ -583,6 +587,66 @@ public QueryTokenStream visitValues(HqlParser.ValuesContext ctx) { return builder; } + @Override + public QueryTokenStream visitConflictClause(HqlParser.ConflictClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.ON())); + builder.append(QueryTokens.expression(ctx.CONFLICT())); + + if (ctx.conflictTarget() != null) { + builder.appendExpression(visit(ctx.conflictTarget())); + } + + builder.append(QueryTokens.expression(ctx.DO())); + builder.appendExpression(visit(ctx.conflictAction())); + + return builder; + } + + @Override + public QueryTokenStream visitConflictTarget(HqlParser.ConflictTargetContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.identifier() != null) { + + builder.append(QueryTokens.expression(ctx.ON())); + builder.append(QueryTokens.expression(ctx.CONSTRAINT())); + builder.appendExpression(visit(ctx.identifier())); + } + + if (!ObjectUtils.isEmpty(ctx.simplePath())) { + + builder.append(TOKEN_OPEN_PAREN); + builder.append(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); + + builder.append(TOKEN_CLOSE_PAREN); + } + + return builder; + } + + @Override + public QueryTokenStream visitConflictAction(HqlParser.ConflictActionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.NOTHING() != null) { + builder.append(QueryTokens.expression(ctx.NOTHING())); + } else { + builder.append(QueryTokens.expression(ctx.UPDATE())); + builder.appendExpression(visit(ctx.setClause())); + + if (ctx.whereClause() != null) { + builder.appendExpression(visit(ctx.whereClause())); + } + } + + return builder; + } + @Override public QueryTokenStream visitInstantiation(HqlParser.InstantiationContext ctx) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 62efc2fdc2..3e459fa26e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -983,6 +983,29 @@ void theRest38() { """); } + @Test // GH-3689 + void insertQueries() { + + assertQuery("insert Person (id, name) values (100L, 'Jane Doe')"); + + assertQuery("insert Person (id, name) values " + // + "(101L, 'J A Doe III'), " + // + "(102L, 'J X Doe'), " + // + "(103L, 'John Doe, Jr')"); + + assertQuery("insert into Partner (id, name) " + // + "select p.id, p.name from Person p "); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT (range) DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO NOTHING"); + } + @Test void hqlQueries() { @@ -1000,15 +1023,7 @@ void hqlQueries() { assertQuery("update versioned Person " + // "set name = :newName " + // "where name = :oldName"); - assertQuery("insert Person (id, name) " + // - "values (100L, 'Jane Doe')"); - assertQuery("insert Person (id, name) " + // - "values (101L, 'J A Doe III'), " + // - "(102L, 'J X Doe'), " + // - "(103L, 'John Doe, Jr')"); - assertQuery("insert into Partner (id, name) " + // - "select p.id, p.name " + // - "from Person p "); + assertQuery("select p " + // "from Person p " + // "where p.name like 'Joe'"); From 77323664d7d3852ac8b586aa61d832891fb45ede Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Nov 2024 16:49:10 +0100 Subject: [PATCH 3/4] Add support for missing clauses and functions. --- .../data/jpa/repository/query/Hql.g4 | 589 ++++++- .../repository/query/HqlQueryRenderer.java | 1486 +++++++++++++---- .../query/HqlQueryRendererTests.java | 9 +- .../query/HqlSpecificationTests.java | 255 ++- 4 files changed, 1925 insertions(+), 414 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 183a1072d2..728d3fe7b2 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 @@ -188,10 +188,6 @@ instantiation : NEW instantiationTarget '(' instantiationArguments ')' ; -alias - : AS? identifier // spec says IDENTIFIER but clearly does NOT mean a reserved word - ; - groupedItem : identifier | INTEGER_LITERAL @@ -354,6 +350,17 @@ dateTimeLiteral | INSTANT ; +/** + * A field that may be extracted from a date, time, or datetime + */ +extractField + : datetimeField + | dayField + | weekField + | timeZoneField + | dateOrTimeField + ; + // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-duration-literals datetimeField : YEAR @@ -368,6 +375,27 @@ datetimeField | EPOCH ; +dayField + : DAY OF MONTH + | DAY OF WEEK + | DAY OF YEAR + ; + +weekField + : WEEK OF MONTH + | WEEK OF YEAR + ; + +timeZoneField + : OFFSET (HOUR | MINUTE)? + | TIMEZONE_HOUR | TIMEZONE_MINUTE + ; + +dateOrTimeField + : DATE + | TIME + ; + // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-binary-literals binaryLiteral : BINARY_LITERAL @@ -416,11 +444,6 @@ primaryExpression // TBD // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-path-expressions -identificationVariable - : identifier - | simplePath - ; - path : treatedPath pathContinutation? | generalPathFragment @@ -467,112 +490,498 @@ caseWhenPredicateClause ; // Functions -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-exp-functions +/** + * A function invocation that may occur in an arbitrary expression + */ function - : functionName '(' (functionArguments | ASTERISK)? ')' pathContinutation? filterClause? withinGroup? overClause? # GenericFunction - | functionName '(' subquery ')' # FunctionWithSubquery - | castFunction # CastFunctionInvocation - | extractFunction # ExtractFunctionInvocation - | trimFunction # TrimFunctionInvocation - | everyFunction # EveryFunctionInvocation - | anyFunction # AnyFunctionInvocation - | treatedPath # TreatedPathInvocation + : standardFunction # StandardFunctionInvocation + | aggregateFunction # AggregateFunctionInvocation + | collectionSizeFunction # CollectionSizeFunctionInvocation + | collectionAggregateFunction # CollectionAggregateFunctionInvocation + | collectionFunctionMisuse # CollectionFunctionMisuseInvocation + | jpaNonstandardFunction # JpaNonstandardFunctionInvocation + | columnFunction # ColumnFunctionInvocation + | genericFunction # GenericFunctionInvocation ; -functionArguments - : DISTINCT? expressionOrPredicate (',' expressionOrPredicate)* +/** + * Any function with an irregular syntax for the argument list + * + * These are all inspired by the syntax of ANSI SQL + */ +standardFunction + : castFunction + | treatedPath + | extractFunction + | truncFunction + | formatFunction + | collateFunction + | substringFunction + | overlayFunction + | trimFunction + | padFunction + | positionFunction + | currentDateFunction + | currentTimeFunction + | currentTimestampFunction + | instantFunction + | localDateFunction + | localTimeFunction + | localDateTimeFunction + | offsetDateTimeFunction + | cube + | rollup ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-aggregate-functions-filter -filterClause - : FILTER '(' whereClause ')' +/** + * The 'cast()' function for typecasting + */ +castFunction + : CAST '(' expression AS castTarget ')' ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-aggregate-functions-orderedset -withinGroup - : WITHIN GROUP '(' orderByClause ')' +/** + * The target type for a typecast: a typename, together with length or precision/scale + */ +castTarget + : castTargetType ('(' INTEGER_LITERAL (',' INTEGER_LITERAL)? ')')? ; -overClause - : OVER '(' partitionClause? orderByClause? frameClause? ')' +/** + * The name of the target type in a typecast + * + * Like the 'entityName' rule, we have a specialized dotIdentifierSequence rule + */ +castTargetType + returns [String fullTargetName] + : (i=identifier { $fullTargetName = _localctx.i.getText(); }) ('.' c=identifier { $fullTargetName += ("." + _localctx.c.getText() ); })* ; -partitionClause - : PARTITION BY expression (',' expression)* +/** + * The two formats for the 'substring() function: one defined by JPQL, the other by ANSI SQL + */ +substringFunction + : SUBSTRING '(' expression ',' substringFunctionStartArgument (',' substringFunctionLengthArgument)? ')' + | SUBSTRING '(' expression FROM substringFunctionStartArgument (FOR substringFunctionLengthArgument)? ')' ; -frameClause - : (RANGE|ROWS|GROUPS) frameStart frameExclusion? - | (RANGE|ROWS|GROUPS) BETWEEN frameStart AND frameEnd frameExclusion? +substringFunctionStartArgument + : expression ; -frameStart - : UNBOUNDED PRECEDING # UnboundedPrecedingFrameStart - | expression PRECEDING # ExpressionPrecedingFrameStart - | CURRENT ROW # CurrentRowFrameStart - | expression FOLLOWING # ExpressionFollowingFrameStart +substringFunctionLengthArgument + : expression ; -frameExclusion - : EXCLUDE CURRENT ROW # CurrentRowFrameExclusion - | EXCLUDE GROUP # GroupFrameExclusion - | EXCLUDE TIES # TiesFrameExclusion - | EXCLUDE NO OTHERS # NoOthersFrameExclusion +/** + * The ANSI SQL-style 'trim()' function + */ +trimFunction + : TRIM '(' trimSpecification? trimCharacter? FROM? expression ')' ; -frameEnd - : expression PRECEDING # ExpressionPrecedingFrameEnd - | CURRENT ROW # CurrentRowFrameEnd - | expression FOLLOWING # ExpressionFollowingFrameEnd - | UNBOUNDED FOLLOWING # UnboundedFollowingFrameEnd +trimSpecification + : LEADING + | TRAILING + | BOTH ; -// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-functions -castFunction - : CAST '(' expression AS castTarget ')' +trimCharacter + : stringLiteral + | parameter ; -castTarget - : castTargetType ('(' INTEGER_LITERAL (',' INTEGER_LITERAL)? ')')? +/** + * A 'pad()' function inspired by 'trim()' + */ +padFunction + : PAD '(' expression WITH padLength padSpecification padCharacter? ')' ; -castTargetType - returns [String fullTargetName] - : (i=identifier { $fullTargetName = _localctx.i.getText(); }) ('.' c=identifier { $fullTargetName += ("." + _localctx.c.getText() ); })* +padSpecification + : LEADING + | TRAILING + ; + +padCharacter + : stringLiteral + ; + +padLength + : expression + ; + +/** + * The ANSI SQL-style 'position()' function + */ +positionFunction + : POSITION '(' positionFunctionPatternArgument IN positionFunctionStringArgument ')' + ; + +positionFunctionPatternArgument + : expression + ; + +positionFunctionStringArgument + : expression + ; + +/** + * The ANSI SQL-style 'overlay()' function + */ +overlayFunction + : OVERLAY '(' overlayFunctionStringArgument PLACING overlayFunctionReplacementArgument FROM overlayFunctionStartArgument (FOR overlayFunctionLengthArgument)? ')' + ; + +overlayFunctionStringArgument + : expression + ; + +overlayFunctionReplacementArgument + : expression + ; + +overlayFunctionStartArgument + : expression + ; + +overlayFunctionLengthArgument + : expression + ; + +/** + * The deprecated current_date function required by JPQL + */ +currentDateFunction + : CURRENT_DATE ('(' ')')? + | CURRENT DATE + ; + +/** + * The deprecated current_time function required by JPQL + */ +currentTimeFunction + : CURRENT_TIME ('(' ')')? + | CURRENT TIME + ; + +/** + * The deprecated current_timestamp function required by JPQL + */ +currentTimestampFunction + : CURRENT_TIMESTAMP ('(' ')')? + | CURRENT TIMESTAMP + ; + +/** + * The instant function, and deprecated current_instant function + */ +instantFunction + : CURRENT_INSTANT ('(' ')')? //deprecated legacy syntax + | INSTANT + ; + +/** + * The 'local datetime' function (or literal if you prefer) + */ +localDateTimeFunction + : LOCAL_DATETIME ('(' ')')? + | LOCAL DATETIME + ; + +/** + * The 'offset datetime' function (or literal if you prefer) + */ +offsetDateTimeFunction + : OFFSET_DATETIME ('(' ')')? + | OFFSET DATETIME + ; + +/** + * The 'local date' function (or literal if you prefer) + */ +localDateFunction + : LOCAL_DATE ('(' ')')? + | LOCAL DATE + ; + +/** + * The 'local time' function (or literal if you prefer) + */ +localTimeFunction + : LOCAL_TIME ('(' ')')? + | LOCAL TIME + ; + +/** + * The 'format()' function for formatting dates and times according to a pattern + */ +formatFunction + : FORMAT '(' expression AS format ')' + ; + +/** + * The name of a database-defined collation + * + * Certain databases allow a period in a collation name + */ +collation + : simplePath + ; + +/** + * The special 'collate()' functions + */ +collateFunction + : COLLATE '(' expression AS collation ')' + ; + +/** + * The 'cube()' function specific to the 'group by' clause + */ +cube + : CUBE '(' expressionOrPredicate (',' expressionOrPredicate)* ')' ; +/** + * The 'rollup()' function specific to the 'group by' clause + */ +rollup + : ROLLUP '(' expressionOrPredicate (',' expressionOrPredicate)* ')' + ; + +/** + * A format pattern, with a syntax inspired by by java.time.format.DateTimeFormatter + * + * see 'Dialect.appendDatetimeFormat()' + */ +format + : stringLiteral + ; + +/** + * The 'extract()' function for extracting fields of dates, times, and datetimes + */ extractFunction - : EXTRACT '(' expression FROM expression ')' - | dateTimeFunction '(' expression ')' + : EXTRACT '(' extractField FROM expression ')' + | datetimeField '(' expression ')' ; -trimFunction - : TRIM '(' (LEADING | TRAILING | BOTH)? stringLiteral? FROM? expression ')' +/** + * The 'trunc()' function for truncating both numeric and datetime values + */ +truncFunction + : (TRUNC | TRUNCATE) '(' expression (',' (datetimeField | expression))? ')' ; -dateTimeFunction - : d=(YEAR - | MONTH - | DAY - | WEEK - | QUARTER - | HOUR - | MINUTE - | SECOND - | NANOSECOND - | EPOCH) +/** + * A syntax for calling user-defined or native database functions, required by JPQL + */ +jpaNonstandardFunction + : FUNCTION '(' jpaNonstandardFunctionName (AS castTarget)? (',' genericFunctionArguments)? ')' + ; + +/** + * The name of a user-defined or native database function, given as a quoted string + */ +jpaNonstandardFunctionName + : stringLiteral + | identifier + ; + +columnFunction + : COLUMN '(' path '.' jpaNonstandardFunctionName (AS castTarget)? ')' + ; + +/** + * Any function invocation that follows the regular syntax + * + * The function name, followed by a parenthesized list of ','-separated expressions + */ +genericFunction + : genericFunctionName '(' (genericFunctionArguments | ASTERISK)? ')' pathContinutation? + nthSideClause? nullsClause? withinGroupClause? filterClause? overClause? + ; + +/** + * The name of a generic function, which may contain periods and quoted identifiers + * + * Names of generic functions are resolved against the SqmFunctionRegistry + */ +genericFunctionName + : simplePath ; +/** + * The arguments of a generic function + */ +genericFunctionArguments + : (DISTINCT | datetimeField ',')? expressionOrPredicate (',' expressionOrPredicate)* + ; + +/** + * The special 'size()' function defined by JPQL + */ +collectionSizeFunction + : SIZE '(' path ')' + ; + +/** + * Special rule for 'max(elements())`, 'avg(keys())', 'sum(indices())`, etc., as defined by HQL + * Also the deprecated 'maxindex()', 'maxelement()', 'minindex()', 'minelement()' functions from old HQL + */ +collectionAggregateFunction + : (MAX|MIN|SUM|AVG) '(' elementsValuesQuantifier '(' path ')' ')' # ElementAggregateFunction + | (MAX|MIN|SUM|AVG) '(' indicesKeysQuantifier '(' path ')' ')' # IndexAggregateFunction + | (MAXELEMENT|MINELEMENT) '(' path ')' # ElementAggregateFunction + | (MAXINDEX|MININDEX) '(' path ')' # IndexAggregateFunction + ; + +/** + * To accommodate the misuse of elements() and indices() in the select clause + * + * (At some stage in the history of HQL, someone mixed them up with value() and index(), + * and so we have tests that insist they're interchangeable. Ugh.) + */ +collectionFunctionMisuse + : elementsValuesQuantifier '(' path ')' + | indicesKeysQuantifier '(' path ')' + ; + +/** + * The special 'every()', 'all()', 'any()' and 'some()' functions defined by HQL + * + * May be applied to a subquery or collection reference, or may occur as an aggregate function in the 'select' clause + */ +aggregateFunction + : everyFunction + | anyFunction + | listaggFunction + ; + +/** + * The functions 'every()' and 'all()' are synonyms + */ everyFunction - : every=(EVERY | ALL) '(' predicate ')' - | every=(EVERY | ALL) '(' subquery ')' - | every=(EVERY | ALL) (ELEMENTS | INDICES) '(' simplePath ')' + : everyAllQuantifier '(' predicate ')' filterClause? overClause? + | everyAllQuantifier '(' subquery ')' + | everyAllQuantifier collectionQuantifier '(' simplePath ')' ; +/** + * The functions 'any()' and 'some()' are synonyms + */ anyFunction - : any=(ANY | SOME) '(' predicate ')' - | any=(ANY | SOME) '(' subquery ')' - | any=(ANY | SOME) (ELEMENTS | INDICES) '(' simplePath ')' + : anySomeQuantifier '(' predicate ')' filterClause? overClause? + | anySomeQuantifier '(' subquery ')' + | anySomeQuantifier collectionQuantifier '(' simplePath ')' + ; + +everyAllQuantifier + : EVERY + | ALL + ; + +anySomeQuantifier + : ANY + | SOME + ; + +/** + * The 'listagg()' ordered set-aggregate function + */ +listaggFunction + : LISTAGG '(' DISTINCT? expressionOrPredicate ',' expressionOrPredicate onOverflowClause? ')' + withinGroupClause? filterClause? overClause? + ; + +/** + * A 'on overflow' clause: what to do when the text data type used for 'listagg' overflows + */ +onOverflowClause + : ON OVERFLOW (ERROR | TRUNCATE expression? (WITH|WITHOUT) COUNT) + ; + +/** + * A 'within group' clause: defines the order in which the ordered set-aggregate function should work + */ +withinGroupClause + : WITHIN GROUP '(' orderByClause ')' + ; + +/** + * A 'filter' clause: a restriction applied to an aggregate function + */ +filterClause + : FILTER '(' whereClause ')' + ; + +/** + * A `nulls` clause: what should a value access window function do when encountering a `null` + */ +nullsClause + : RESPECT NULLS + | IGNORE NULLS + ; + +/** + * A `nulls` clause: what should a value access window function do when encountering a `null` + */ +nthSideClause + : FROM FIRST + | FROM LAST + ; + +/** + * A 'over' clause: the specification of a window within which the function should act + */ +overClause + : OVER '(' partitionClause? orderByClause? frameClause? ')' + ; + +/** + * A 'partition' clause: the specification the group within which a function should act in a window + */ +partitionClause + : PARTITION BY expression (',' expression)* + ; + +/** + * A 'frame' clause: the specification the content of the window + */ +frameClause + : (RANGE|ROWS|GROUPS) frameStart frameExclusion? + | (RANGE|ROWS|GROUPS) BETWEEN frameStart AND frameEnd frameExclusion? + ; + +/** + * The start of the window content + */ +frameStart + : CURRENT ROW + | UNBOUNDED PRECEDING + | expression PRECEDING + | expression FOLLOWING + ; + +/** + * The end of the window content + */ +frameEnd + : CURRENT ROW + | UNBOUNDED FOLLOWING + | expression PRECEDING + | expression FOLLOWING + ; + +/** + * A 'exclusion' clause: the specification what to exclude from the window content + */ +frameExclusion + : EXCLUDE CURRENT ROW + | EXCLUDE GROUP + | EXCLUDE TIES + | EXCLUDE NO OTHERS ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-treat-type @@ -608,6 +1017,21 @@ expressionOrPredicate | predicate ; +collectionQuantifier + : elementsValuesQuantifier + | indicesKeysQuantifier + ; + +elementsValuesQuantifier + : ELEMENTS + | VALUES + ; + +indicesKeysQuantifier + : INDICES + | KEYS + ; + // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-relational-comparisons // NOTE: The TIP shows that "!=" is also supported. Hibernate's source code shows that "^=" is another NOT_EQUALS option as well. relationalExpression @@ -691,10 +1115,6 @@ identifier : reservedWord ; -character - : CHARACTER - ; - functionName : reservedWord ('.' reservedWord)* ; @@ -938,7 +1358,11 @@ CURRENT_DATE : C U R R E N T '_' D A T E; CURRENT_INSTANT : C U R R E N T '_' I N S T A N T; CURRENT_TIME : C U R R E N T '_' T I M E; CURRENT_TIMESTAMP : C U R R E N T '_' T I M E S T A M P; +CONFLICT : C O N F L I C T; +CONSTRAINT : C O N S T R A I N T; +COLUMN : C O L U M N; CYCLE : C Y C L E; +DO : D O; DATE : D A T E; DATETIME : D A T E T I M E ; DAY : D A Y; @@ -994,6 +1418,7 @@ INTO : I N T O; IS : I S; JOIN : J O I N; KEY : K E Y; +KEYS : K E Y S; LAST : L A S T; LATERAL : L A T E R A L; LEADING : L E A D I N G; @@ -1026,6 +1451,7 @@ 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; @@ -1109,4 +1535,3 @@ BINARY_LITERAL : [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 528cdfe263..2ef49b95ff 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 @@ -24,6 +24,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. @@ -661,20 +662,6 @@ public QueryTokenStream visitInstantiation(HqlParser.InstantiationContext ctx) { return builder; } - @Override - public QueryTokenStream visitAlias(HqlParser.AliasContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - - builder.append(visit(ctx.identifier())); - - return builder; - } - @Override public QueryTokenStream visitGroupedItem(HqlParser.GroupedItemContext ctx) { @@ -1145,30 +1132,105 @@ public QueryTokenStream visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ct public QueryTokenStream visitDatetimeField(HqlParser.DatetimeFieldContext ctx) { if (ctx.YEAR() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.YEAR())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.YEAR())); } else if (ctx.MONTH() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.MONTH())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.MONTH())); } else if (ctx.DAY() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.DAY())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.DAY())); } else if (ctx.WEEK() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.WEEK())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.WEEK())); } else if (ctx.QUARTER() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.QUARTER())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.QUARTER())); } else if (ctx.HOUR() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.HOUR())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.HOUR())); } else if (ctx.MINUTE() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.MINUTE())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.MINUTE())); } else if (ctx.SECOND() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.SECOND())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.SECOND())); } else if (ctx.NANOSECOND() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.NANOSECOND())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.NANOSECOND())); } else if (ctx.EPOCH() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.EPOCH())); + return QueryRendererBuilder.from(QueryTokens.token(ctx.EPOCH())); } else { return QueryTokenStream.empty(); } } + @Override + public QueryTokenStream visitDayField(HqlParser.DayFieldContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.DAY())); + builder.append(QueryTokens.expression(ctx.OF())); + + if (ctx.MONTH() != null) { + builder.append(QueryTokens.expression(ctx.MONTH())); + } + + if (ctx.WEEK() != null) { + builder.append(QueryTokens.expression(ctx.WEEK())); + } + + if (ctx.YEAR() != null) { + builder.append(QueryTokens.expression(ctx.YEAR())); + } + + return builder; + } + + @Override + public QueryTokenStream visitWeekField(HqlParser.WeekFieldContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.WEEK())); + builder.append(QueryTokens.expression(ctx.OF())); + + if (ctx.MONTH() != null) { + builder.append(QueryTokens.expression(ctx.MONTH())); + } + + if (ctx.YEAR() != null) { + builder.append(QueryTokens.expression(ctx.YEAR())); + } + + return builder; + } + + @Override + public QueryTokenStream visitTimeZoneField(HqlParser.TimeZoneFieldContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.OFFSET() != null) { + builder.append(QueryTokens.expression(ctx.OFFSET())); + + if (ctx.HOUR() != null) { + builder.append(QueryTokens.expression(ctx.HOUR())); + } + + if (ctx.MINUTE() != null) { + builder.append(QueryTokens.expression(ctx.MINUTE())); + } + } + + if (ctx.TIMEZONE_HOUR() != null) { + builder.append(QueryTokens.expression(ctx.TIMEZONE_HOUR())); + } + + if (ctx.TIMEZONE_HOUR() != null) { + builder.append(QueryTokens.expression(ctx.TIMEZONE_MINUTE())); + } + + return builder; + } + + @Override + public QueryTokenStream visitDateOrTimeField(HqlParser.DateOrTimeFieldContext ctx) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.DATE() != null ? ctx.DATE() : ctx.TIME())); + } + @Override public QueryTokenStream visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { @@ -1331,7 +1393,7 @@ public QueryTokenStream visitToDurationExpression(HqlParser.ToDurationExpression QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(visit(ctx.expression())); - builder.append(visit(ctx.datetimeField())); + builder.appendExpression(visit(ctx.datetimeField())); return builder; } @@ -1343,7 +1405,7 @@ public QueryTokenStream visitFromDurationExpression(HqlParser.FromDurationExpres builder.append(visit(ctx.expression())); builder.append(QueryTokens.expression(ctx.BY())); - builder.append(visit(ctx.datetimeField())); + builder.appendExpression(visit(ctx.datetimeField())); return builder; } @@ -1369,263 +1431,579 @@ public QueryTokenStream visitFunctionExpression(HqlParser.FunctionExpressionCont } @Override - public QueryTokenStream visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext ctx) { - return visit(ctx.generalPathFragment()); + public QueryTokenStream visitStandardFunctionInvocation(HqlParser.StandardFunctionInvocationContext ctx) { + return visit(ctx.standardFunction()); } @Override - public QueryTokenStream visitIdentificationVariable(HqlParser.IdentificationVariableContext ctx) { - - if (ctx.identifier() != null) { - return visit(ctx.identifier()); - } else if (ctx.simplePath() != null) { - return visit(ctx.simplePath()); - } else { - return QueryTokenStream.empty(); - } + public QueryTokenStream visitAggregateFunctionInvocation(HqlParser.AggregateFunctionInvocationContext ctx) { + return visit(ctx.aggregateFunction()); } @Override - public QueryTokenStream visitPath(HqlParser.PathContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitCollectionSizeFunctionInvocation(HqlParser.CollectionSizeFunctionInvocationContext ctx) { + return visit(ctx.collectionSizeFunction()); + } - if (ctx.treatedPath() != null) { + @Override + public QueryTokenStream visitCollectionAggregateFunctionInvocation( + HqlParser.CollectionAggregateFunctionInvocationContext ctx) { + return visit(ctx.collectionAggregateFunction()); + } - builder.append(visit(ctx.treatedPath())); + @Override + public QueryTokenStream visitCollectionFunctionMisuseInvocation( + HqlParser.CollectionFunctionMisuseInvocationContext ctx) { + return visit(ctx.collectionFunctionMisuse()); + } - if (ctx.pathContinutation() != null) { - builder.append(visit(ctx.pathContinutation())); - } - } else if (ctx.generalPathFragment() != null) { - builder.append(visit(ctx.generalPathFragment())); - } + @Override + public QueryTokenStream visitJpaNonstandardFunctionInvocation(HqlParser.JpaNonstandardFunctionInvocationContext ctx) { + return visit(ctx.jpaNonstandardFunction()); + } - return builder; + @Override + public QueryTokenStream visitColumnFunctionInvocation(HqlParser.ColumnFunctionInvocationContext ctx) { + return visit(ctx.columnFunction()); } @Override - public QueryTokenStream visitGeneralPathFragment(HqlParser.GeneralPathFragmentContext ctx) { + public QueryTokenStream visitGenericFunctionInvocation(HqlParser.GenericFunctionInvocationContext ctx) { + return visit(ctx.genericFunction()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + @Override + public QueryTokenStream visitStandardFunction(HqlParser.StandardFunctionContext ctx) { - builder.append(visit(ctx.simplePath())); + if (ctx.castFunction() != null) { + return visit(ctx.castFunction()); + } - if (ctx.indexedPathAccessFragment() != null) { - builder.append(visit(ctx.indexedPathAccessFragment())); + if (ctx.treatedPath() != null) { + return visit(ctx.treatedPath()); } - return builder; - } + if (ctx.extractFunction() != null) { + return visit(ctx.extractFunction()); + } - @Override - public QueryTokenStream visitIndexedPathAccessFragment(HqlParser.IndexedPathAccessFragmentContext ctx) { + if (ctx.truncFunction() != null) { + return visit(ctx.truncFunction()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.formatFunction() != null) { + return visit(ctx.formatFunction()); + } - builder.append(TOKEN_OPEN_SQUARE_BRACKET); - builder.appendInline(visit(ctx.expression())); - builder.append(TOKEN_CLOSE_SQUARE_BRACKET); + if (ctx.collateFunction() != null) { + return visit(ctx.collateFunction()); + } - if (ctx.generalPathFragment() != null) { + if (ctx.substringFunction() != null) { + return visit(ctx.substringFunction()); + } - builder.append(TOKEN_DOT); - builder.append(visit(ctx.generalPathFragment())); + if (ctx.overlayFunction() != null) { + return visit(ctx.overlayFunction()); } - return builder; - } + if (ctx.trimFunction() != null) { + return visit(ctx.trimFunction()); + } - @Override - public QueryTokenStream visitSimplePath(HqlParser.SimplePathContext ctx) { + if (ctx.padFunction() != null) { + return visit(ctx.padFunction()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.positionFunction() != null) { + return visit(ctx.positionFunction()); + } - builder.append(visit(ctx.identifier())); + if (ctx.currentDateFunction() != null) { + return visit(ctx.currentDateFunction()); + } - if (!ctx.simplePathElement().isEmpty()) { - builder.append(TOKEN_DOT); + if (ctx.currentTimeFunction() != null) { + return visit(ctx.currentTimeFunction()); } - builder.append(QueryTokenStream.concat(ctx.simplePathElement(), this::visit, TOKEN_DOT)); + if (ctx.currentTimestampFunction() != null) { + return visit(ctx.currentTimestampFunction()); + } - return builder; - } + if (ctx.instantFunction() != null) { + return visit(ctx.instantFunction()); + } - @Override - public QueryTokenStream visitSimplePathElement(HqlParser.SimplePathElementContext ctx) { + if (ctx.localDateFunction() != null) { + return visit(ctx.localDateFunction()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.localTimeFunction() != null) { + return visit(ctx.localTimeFunction()); + } - builder.append(visit(ctx.identifier())); + if (ctx.localDateTimeFunction() != null) { + return visit(ctx.localDateTimeFunction()); + } - return builder; - } + if (ctx.offsetDateTimeFunction() != null) { + return visit(ctx.offsetDateTimeFunction()); + } - @Override - public QueryTokenStream visitCaseList(HqlParser.CaseListContext ctx) { + if (ctx.cube() != null) { + return visit(ctx.cube()); + } - if (ctx.simpleCaseExpression() != null) { - return visit(ctx.simpleCaseExpression()); - } else if (ctx.searchedCaseExpression() != null) { - return visit(ctx.searchedCaseExpression()); - } else { - return QueryTokenStream.empty(); + if (ctx.rollup() != null) { + return visit(ctx.rollup()); } + + return QueryTokenStream.empty(); } @Override - public QueryTokenStream visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { + public QueryTokenStream visitSubstringFunction(HqlParser.SubstringFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.CASE())); - builder.append(visit(ctx.expressionOrPredicate(0))); + builder.append(QueryTokens.token(ctx.SUBSTRING())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.expression())); - ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { - builder.append(visit(caseWhenExpressionClauseContext)); - }); + if (ctx.FROM() == null) { + builder.append(TOKEN_COMMA); + } else { + builder.append(QueryTokens.expression(ctx.FROM())); + } - if (ctx.ELSE() != null) { + builder.append(visit(ctx.substringFunctionStartArgument())); - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.append(visit(ctx.expressionOrPredicate(1))); + if (ctx.substringFunctionLengthArgument() != null) { + if (ctx.FOR() == null) { + builder.append(TOKEN_COMMA); + } else { + builder.append(QueryTokens.expression(ctx.FOR())); + } + + builder.append(visit(ctx.substringFunctionLengthArgument())); } - builder.append(QueryTokens.expression(ctx.END())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { + public QueryTokenStream visitSubstringFunctionStartArgument(HqlParser.SubstringFunctionStartArgumentContext ctx) { + return visit(ctx.expression()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + @Override + public QueryTokenStream visitSubstringFunctionLengthArgument(HqlParser.SubstringFunctionLengthArgumentContext ctx) { + return visit(ctx.expression()); + } - builder.append(QueryTokens.expression(ctx.CASE())); + @Override + public QueryTokenStream visitPadFunction(HqlParser.PadFunctionContext ctx) { - builder.append(QueryTokenStream.concat(ctx.caseWhenPredicateClause(), this::visit, TOKEN_SPACE)); + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.ELSE() != null) { + builder.append(QueryTokens.token(ctx.PAD())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.WITH())); + builder.appendExpression(visit(ctx.padLength())); - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); + if (ctx.padCharacter() != null) { + builder.appendExpression(visit(ctx.padSpecification())); + builder.appendInline(visit(ctx.padCharacter())); + } else { + builder.append(visit(ctx.padSpecification())); } - builder.append(QueryTokens.expression(ctx.END())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitCaseWhenExpressionClause(HqlParser.CaseWhenExpressionClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitPadSpecification(HqlParser.PadSpecificationContext ctx) { + return QueryRendererBuilder.from(QueryTokens.token(ctx.LEADING() != null ? ctx.LEADING() : ctx.TRAILING())); + } - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); + @Override + public QueryTokenStream visitPadCharacter(HqlParser.PadCharacterContext ctx) { + return visit(ctx.stringLiteral()); + } - return builder; + @Override + public QueryTokenStream visitPadLength(HqlParser.PadLengthContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitCaseWhenPredicateClause(HqlParser.CaseWhenPredicateClauseContext ctx) { + public QueryTokenStream visitPositionFunction(HqlParser.PositionFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.predicate())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); + builder.append(QueryTokens.token(ctx.POSITION())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.positionFunctionPatternArgument())); + builder.append(QueryTokens.expression(ctx.IN())); + builder.appendInline(visit(ctx.positionFunctionStringArgument())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitGenericFunction(HqlParser.GenericFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.append(visit(ctx.functionName())); - nested.append(TOKEN_OPEN_PAREN); - - if (ctx.functionArguments() != null) { - nested.appendInline(visit(ctx.functionArguments())); - } else if (ctx.ASTERISK() != null) { - nested.append(QueryTokens.token(ctx.ASTERISK())); - } - - nested.append(TOKEN_CLOSE_PAREN); - - builder.append(nested); - - if (ctx.pathContinutation() != null) { - builder.appendInline(visit(ctx.pathContinutation())); - } - - if (ctx.filterClause() != null) { - builder.appendExpression(visit(ctx.filterClause())); - } - - if (ctx.withinGroup() != null) { - builder.appendExpression(visit(ctx.withinGroup())); - } - - if (ctx.overClause() != null) { - builder.appendExpression(visit(ctx.overClause())); - } + public QueryTokenStream visitPositionFunctionPatternArgument(HqlParser.PositionFunctionPatternArgumentContext ctx) { + return visit(ctx.expression()); + } - return builder; + @Override + public QueryTokenStream visitPositionFunctionStringArgument(HqlParser.PositionFunctionStringArgumentContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitFunctionWithSubquery(HqlParser.FunctionWithSubqueryContext ctx) { + public QueryTokenStream visitOverlayFunction(HqlParser.OverlayFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.functionName())); + builder.append(QueryTokens.token(ctx.OVERLAY())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); + builder.append(visit(ctx.overlayFunctionStringArgument())); + builder.append(QueryTokens.expression(ctx.PLACING())); + builder.append(visit(ctx.overlayFunctionReplacementArgument())); + builder.append(QueryTokens.expression(ctx.FROM())); + builder.append(visit(ctx.overlayFunctionStartArgument())); + + if (ctx.overlayFunctionLengthArgument() != null) { + builder.append(QueryTokens.expression(ctx.FOR())); + builder.append(visit(ctx.overlayFunctionLengthArgument())); + } builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitCastFunctionInvocation(HqlParser.CastFunctionInvocationContext ctx) { - return visit(ctx.castFunction()); + public QueryTokenStream visitOverlayFunctionStringArgument(HqlParser.OverlayFunctionStringArgumentContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitExtractFunctionInvocation(HqlParser.ExtractFunctionInvocationContext ctx) { - return visit(ctx.extractFunction()); + public QueryTokenStream visitOverlayFunctionReplacementArgument( + HqlParser.OverlayFunctionReplacementArgumentContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitTrimFunctionInvocation(HqlParser.TrimFunctionInvocationContext ctx) { - return visit(ctx.trimFunction()); + public QueryTokenStream visitOverlayFunctionStartArgument(HqlParser.OverlayFunctionStartArgumentContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitEveryFunctionInvocation(HqlParser.EveryFunctionInvocationContext ctx) { - return visit(ctx.everyFunction()); + public QueryTokenStream visitOverlayFunctionLengthArgument(HqlParser.OverlayFunctionLengthArgumentContext ctx) { + return visit(ctx.expression()); } @Override - public QueryTokenStream visitAnyFunctionInvocation(HqlParser.AnyFunctionInvocationContext ctx) { - return visit(ctx.anyFunction()); - } + public QueryTokenStream visitCurrentDateFunction(HqlParser.CurrentDateFunctionContext ctx) { - @Override - public QueryTokenStream visitTreatedPathInvocation(HqlParser.TreatedPathInvocationContext ctx) { - return visit(ctx.treatedPath()); + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT_DATE() != null) { + builder.append(QueryTokens.token(ctx.CURRENT_DATE())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.DATE())); + } + + return builder; + } + + @Override + public QueryTokenStream visitCurrentTimeFunction(HqlParser.CurrentTimeFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT_TIME() != null) { + builder.append(QueryTokens.token(ctx.CURRENT_TIME())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.TIME())); + } + + return builder; + } + + @Override + public QueryTokenStream visitCurrentTimestampFunction(HqlParser.CurrentTimestampFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT_TIMESTAMP() != null) { + builder.append(QueryTokens.token(ctx.CURRENT_TIMESTAMP())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.TIMESTAMP())); + } + + return builder; + } + + @Override + public QueryTokenStream visitInstantFunction(HqlParser.InstantFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT_INSTANT() != null) { + builder.append(QueryTokens.token(ctx.CURRENT_INSTANT())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.INSTANT())); + } + + return builder; + } + + @Override + public QueryTokenStream visitLocalDateTimeFunction(HqlParser.LocalDateTimeFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.LOCAL_DATETIME() != null) { + builder.append(QueryTokens.token(ctx.LOCAL_DATETIME())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.LOCAL())); + builder.append(QueryTokens.expression(ctx.DATETIME())); + } + + return builder; + } + + @Override + public QueryTokenStream visitOffsetDateTimeFunction(HqlParser.OffsetDateTimeFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.OFFSET_DATETIME() != null) { + builder.append(QueryTokens.token(ctx.OFFSET_DATETIME())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.OFFSET())); + builder.append(QueryTokens.expression(ctx.DATETIME())); + } + + return builder; + } + + @Override + public QueryTokenStream visitLocalDateFunction(HqlParser.LocalDateFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.LOCAL_DATE() != null) { + builder.append(QueryTokens.token(ctx.LOCAL_DATE())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.LOCAL())); + builder.append(QueryTokens.expression(ctx.DATE())); + } + + return builder; + } + + @Override + public QueryTokenStream visitLocalTimeFunction(HqlParser.LocalTimeFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.LOCAL_TIME() != null) { + builder.append(QueryTokens.token(ctx.LOCAL_TIME())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } else { + builder.append(QueryTokens.expression(ctx.LOCAL())); + builder.append(QueryTokens.expression(ctx.TIME())); + } + + return builder; + } + + @Override + public QueryTokenStream visitFormatFunction(HqlParser.FormatFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.FORMAT())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.appendInline(visit(ctx.format())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitCollation(HqlParser.CollationContext ctx) { + return visit(ctx.simplePath()); + } + + @Override + public QueryTokenStream visitCollateFunction(HqlParser.CollateFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.COLLATE())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.appendInline(visit(ctx.collation())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitCube(HqlParser.CubeContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.CUBE())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitRollup(HqlParser.RollupContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.ROLLUP())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitFormat(HqlParser.FormatContext ctx) { + return visit(ctx.stringLiteral()); + } + + @Override + public QueryTokenStream visitTruncFunction(HqlParser.TruncFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.TRUNC() != null) { + builder.append(QueryTokens.token(ctx.TRUNC())); + } else { + builder.append(QueryTokens.token(ctx.TRUNCATE())); + } + + builder.append(TOKEN_OPEN_PAREN); + + if (ctx.datetimeField() != null) { + builder.append(visit(ctx.expression(0))); + builder.append(TOKEN_COMMA); + builder.append(visit(ctx.datetimeField())); + } else { + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + } + builder.append(TOKEN_CLOSE_PAREN); + + return builder; } @Override - public QueryTokenStream visitFunctionArguments(HqlParser.FunctionArgumentsContext ctx) { + public QueryTokenStream visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.FUNCTION())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.appendInline(visit(ctx.jpaNonstandardFunctionName())); + + if (ctx.castTarget() != null) { + nested.append(QueryTokens.expression(ctx.AS())); + nested.append(visit(ctx.castTarget())); + } + + if (ctx.genericFunctionArguments() != null) { + nested.append(TOKEN_COMMA); + nested.appendInline(visit(ctx.genericFunctionArguments())); + } + + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitJpaNonstandardFunctionName(HqlParser.JpaNonstandardFunctionNameContext ctx) { + + if (ctx.identifier() != null) { + return visit(ctx.identifier()); + } + + return visit(ctx.stringLiteral()); + } + + @Override + public QueryTokenStream visitColumnFunction(HqlParser.ColumnFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.COLUMN())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.appendInline(visit(ctx.path())); + nested.append(TOKEN_DOT); + nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); + + if (ctx.castTarget() != null) { + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); + } + + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitGenericFunctionName(HqlParser.GenericFunctionNameContext ctx) { + return visit(ctx.simplePath()); + } + + @Override + public QueryTokenStream visitGenericFunctionArguments(HqlParser.GenericFunctionArgumentsContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -1633,31 +2011,239 @@ public QueryTokenStream visitFunctionArguments(HqlParser.FunctionArgumentsContex builder.append(QueryTokens.expression(ctx.DISTINCT())); } + if (ctx.datetimeField() != null) { + builder.append(visit(ctx.datetimeField())); + builder.append(TOKEN_COMMA); + } + builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitFilterClause(HqlParser.FilterClauseContext ctx) { + public QueryTokenStream visitCollectionSizeFunction(HqlParser.CollectionSizeFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.FILTER())); + builder.append(QueryTokens.token(ctx.SIZE())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.whereClause())); + builder.appendInline(visit(ctx.path())); builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitWithinGroup(HqlParser.WithinGroupContext ctx) { + public QueryTokenStream visitElementAggregateFunction(HqlParser.ElementAggregateFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.MAXELEMENT() != null || ctx.MINELEMENT() != null) { + builder.append(QueryTokens.token(ctx.MAXELEMENT() != null ? ctx.MAXELEMENT() : ctx.MINELEMENT())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + } else { + + if (ctx.MAX() != null) { + builder.append(QueryTokens.token(ctx.MAX())); + } + if (ctx.MIN() != null) { + builder.append(QueryTokens.token(ctx.MIN())); + } + if (ctx.SUM() != null) { + builder.append(QueryTokens.token(ctx.SUM())); + } + if (ctx.AVG() != null) { + builder.append(QueryTokens.token(ctx.AVG())); + } + + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.elementsValuesQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + + if (ctx.path() != null) { + builder.append(visit(ctx.path())); + } + + builder.append(TOKEN_CLOSE_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } + + return builder; + } + + @Override + public QueryTokenStream visitIndexAggregateFunction(HqlParser.IndexAggregateFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.MAXINDEX() != null || ctx.MININDEX() != null) { + builder.append(QueryTokens.token(ctx.MAXINDEX() != null ? ctx.MAXINDEX() : ctx.MININDEX())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + } else { + + if (ctx.MAX() != null) { + builder.append(QueryTokens.token(ctx.MAX())); + } + if (ctx.MIN() != null) { + builder.append(QueryTokens.token(ctx.MIN())); + } + if (ctx.SUM() != null) { + builder.append(QueryTokens.token(ctx.SUM())); + } + if (ctx.AVG() != null) { + builder.append(QueryTokens.token(ctx.AVG())); + } + + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.indicesKeysQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + + if (ctx.path() != null) { + builder.append(visit(ctx.path())); + } + + builder.append(TOKEN_CLOSE_PAREN); + builder.append(TOKEN_CLOSE_PAREN); + } + + return builder; + } + + @Override + public QueryTokenStream visitCollectionFunctionMisuse(HqlParser.CollectionFunctionMisuseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append( + visit(ctx.elementsValuesQuantifier() != null ? ctx.elementsValuesQuantifier() : ctx.indicesKeysQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitAggregateFunction(HqlParser.AggregateFunctionContext ctx) { + + if (ctx.everyFunction() != null) { + return visit(ctx.everyFunction()); + } + + if (ctx.anyFunction() != null) { + return visit(ctx.anyFunction()); + } + + return visit(ctx.listaggFunction()); + } + + @Override + public QueryTokenStream visitEveryAllQuantifier(HqlParser.EveryAllQuantifierContext ctx) { + + if (ctx.EVERY() != null) { + return QueryRenderer.from(QueryTokens.token(ctx.EVERY())); + } + + return QueryRenderer.from(QueryTokens.token(ctx.ALL())); + } + + @Override + public QueryTokenStream visitAnySomeQuantifier(HqlParser.AnySomeQuantifierContext ctx) { + + if (ctx.ANY() != null) { + return QueryRenderer.from(QueryTokens.token(ctx.ANY())); + } + + return QueryRenderer.from(QueryTokens.token(ctx.SOME())); + } + + @Override + public QueryTokenStream visitListaggFunction(HqlParser.ListaggFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.LISTAGG())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); + + if (ctx.DISTINCT() != null) { + builder.append(QueryTokens.expression(ctx.DISTINCT())); + } + + builder.appendInline(visit(ctx.expressionOrPredicate(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.expressionOrPredicate(1))); + + if (ctx.onOverflowClause() != null) { + builder.appendExpression(visit(ctx.onOverflowClause())); + } + + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.withinGroupClause() != null) { + builder.appendExpression(visit(ctx.withinGroupClause())); + } + + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } + + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } + + return builder; + } + + @Override + public QueryTokenStream visitOnOverflowClause(HqlParser.OnOverflowClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.ON())); + builder.append(QueryTokens.expression(ctx.OVERFLOW())); + + if (ctx.ERROR() != null) { + builder.append(QueryTokens.expression(ctx.ERROR())); + } else { + + builder.append(QueryTokens.expression(ctx.TRUNCATE())); + + if (ctx.expression() != null) { + builder.appendExpression(visit(ctx.expression())); + } + + if (ctx.WITH() != null) { + builder.append(QueryTokens.expression(ctx.WITH())); + } + + if (ctx.WITHOUT() != null) { + builder.append(QueryTokens.expression(ctx.WITHOUT())); + } + + if (ctx.COUNT() != null) { + builder.append(QueryTokens.expression(ctx.COUNT())); + } + } + + return builder; + } + + @Override + public QueryTokenStream visitWithinGroupClause(HqlParser.WithinGroupClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.WITHIN())); builder.append(QueryTokens.expression(ctx.GROUP())); + builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.orderByClause())); builder.append(TOKEN_CLOSE_PAREN); @@ -1666,211 +2252,415 @@ public QueryTokenStream visitWithinGroup(HqlParser.WithinGroupContext ctx) { } @Override - public QueryTokenStream visitOverClause(HqlParser.OverClauseContext ctx) { + public QueryTokenStream visitNullsClause(HqlParser.NullsClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.OVER())); - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.append(TOKEN_OPEN_PAREN); + if (ctx.IGNORE() != null) { + builder.append(QueryTokens.expression(ctx.IGNORE())); + } else { + builder.append(QueryTokens.expression(ctx.RESPECT())); + } - List trees = new ArrayList<>(); + builder.append(QueryTokens.expression(ctx.NULLS())); - if (ctx.partitionClause() != null) { - trees.add(ctx.partitionClause()); + return builder; + } + + @Override + public QueryTokenStream visitNthSideClause(HqlParser.NthSideClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.FROM())); + + if (ctx.FIRST() != null) { + builder.append(QueryTokens.expression(ctx.FIRST())); + } else { + builder.append(QueryTokens.expression(ctx.LAST())); } - if (ctx.orderByClause() != null) { - trees.add(ctx.orderByClause()); + return builder; + } + + @Override + public QueryTokenStream visitFrameStart(HqlParser.FrameStartContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT() != null) { + + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.ROW())); + } else if (ctx.UNBOUNDED() != null) { + builder.append(QueryTokens.expression(ctx.UNBOUNDED())); + builder.append(QueryTokens.expression(ctx.PRECEDING())); + } else { + + builder.appendExpression(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.PRECEDING() != null ? ctx.PRECEDING() : ctx.FOLLOWING())); } - if (ctx.frameClause() != null) { - trees.add(ctx.frameClause()); + return builder; + + } + + @Override + public QueryTokenStream visitFrameEnd(HqlParser.FrameEndContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.CURRENT() != null) { + + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.ROW())); + } else if (ctx.UNBOUNDED() != null) { + builder.append(QueryTokens.expression(ctx.UNBOUNDED())); + builder.append(QueryTokens.expression(ctx.FOLLOWING())); + } else { + + builder.appendExpression(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.PRECEDING() != null ? ctx.PRECEDING() : ctx.FOLLOWING())); + } + + return builder; + } + + @Override + public QueryTokenStream visitFrameExclusion(HqlParser.FrameExclusionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.EXCLUDE())); + + if (ctx.CURRENT() != null) { + builder.append(QueryTokens.expression(ctx.CURRENT())); + builder.append(QueryTokens.expression(ctx.ROW())); + } else if (ctx.GROUP() != null) { + builder.append(QueryTokens.expression(ctx.GROUP())); + } else if (ctx.TIES() != null) { + builder.append(QueryTokens.expression(ctx.TIES())); + } else { + builder.append(QueryTokens.expression(ctx.NO())); + builder.append(QueryTokens.expression(ctx.OTHERS())); + } + + return builder; + } + + @Override + public QueryTokenStream visitCollectionQuantifier(HqlParser.CollectionQuantifierContext ctx) { + + if (ctx.elementsValuesQuantifier() != null) { + return visit(ctx.elementsValuesQuantifier()); + } + + return visit(ctx.indicesKeysQuantifier()); + } + + @Override + public QueryTokenStream visitElementsValuesQuantifier(HqlParser.ElementsValuesQuantifierContext ctx) { + return QueryRenderer.from(QueryTokens.token(ctx.ELEMENTS() != null ? ctx.ELEMENTS() : ctx.VALUES())); + } + + @Override + public QueryTokenStream visitIndicesKeysQuantifier(HqlParser.IndicesKeysQuantifierContext ctx) { + return QueryRenderer.from(QueryTokens.token(ctx.INDICES() != null ? ctx.INDICES() : ctx.KEYS())); + } + + @Override + public QueryTokenStream visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext ctx) { + return visit(ctx.generalPathFragment()); + } + + @Override + public QueryTokenStream visitPath(HqlParser.PathContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.treatedPath() != null) { + + builder.append(visit(ctx.treatedPath())); + + if (ctx.pathContinutation() != null) { + builder.append(visit(ctx.pathContinutation())); + } + } else if (ctx.generalPathFragment() != null) { + builder.append(visit(ctx.generalPathFragment())); + } + + return builder; + } + + @Override + public QueryTokenStream visitGeneralPathFragment(HqlParser.GeneralPathFragmentContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.simplePath())); + + if (ctx.indexedPathAccessFragment() != null) { + builder.append(visit(ctx.indexedPathAccessFragment())); + } + + return builder; + } + + @Override + public QueryTokenStream visitIndexedPathAccessFragment(HqlParser.IndexedPathAccessFragmentContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(TOKEN_OPEN_SQUARE_BRACKET); + builder.appendInline(visit(ctx.expression())); + builder.append(TOKEN_CLOSE_SQUARE_BRACKET); + + if (ctx.generalPathFragment() != null) { + + builder.append(TOKEN_DOT); + builder.append(visit(ctx.generalPathFragment())); + } + + return builder; + } + + @Override + public QueryTokenStream visitSimplePath(HqlParser.SimplePathContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.identifier())); + + if (!ctx.simplePathElement().isEmpty()) { + builder.append(TOKEN_DOT); } - nested.appendInline(QueryTokenStream.concat(trees, this::visit, TOKEN_SPACE)); - nested.append(TOKEN_CLOSE_PAREN); - - builder.appendInline(nested); + builder.append(QueryTokenStream.concat(ctx.simplePathElement(), this::visit, TOKEN_DOT)); return builder; } @Override - public QueryTokenStream visitPartitionClause(HqlParser.PartitionClauseContext ctx) { + public QueryTokenStream visitSimplePathElement(HqlParser.SimplePathElementContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.PARTITION())); - builder.append(QueryTokens.expression(ctx.BY())); - - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + builder.append(visit(ctx.identifier())); return builder; } @Override - public QueryTokenStream visitFrameClause(HqlParser.FrameClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitCaseList(HqlParser.CaseListContext ctx) { - if (ctx.RANGE() != null) { - builder.append(QueryTokens.expression(ctx.RANGE())); - } else if (ctx.ROWS() != null) { - builder.append(QueryTokens.expression(ctx.ROWS())); - } else if (ctx.GROUPS() != null) { - builder.append(QueryTokens.expression(ctx.GROUPS())); + if (ctx.simpleCaseExpression() != null) { + return visit(ctx.simpleCaseExpression()); + } else if (ctx.searchedCaseExpression() != null) { + return visit(ctx.searchedCaseExpression()); + } else { + return QueryTokenStream.empty(); } + } - if (ctx.BETWEEN() != null) { - builder.append(QueryTokens.expression(ctx.BETWEEN())); - } + @Override + public QueryTokenStream visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { - builder.appendExpression(visit(ctx.frameStart())); + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.AND() != null) { + builder.append(QueryTokens.expression(ctx.CASE())); + builder.append(visit(ctx.expressionOrPredicate(0))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.frameEnd())); - } + ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { + builder.append(visit(caseWhenExpressionClauseContext)); + }); - if (ctx.frameExclusion() != null) { - builder.appendExpression(visit(ctx.frameExclusion())); + if (ctx.ELSE() != null) { + + builder.append(QueryTokens.expression(ctx.ELSE())); + builder.append(visit(ctx.expressionOrPredicate(1))); } + builder.append(QueryTokens.expression(ctx.END())); + return builder; } @Override - public QueryTokenStream visitUnboundedPrecedingFrameStart(HqlParser.UnboundedPrecedingFrameStartContext ctx) { + public QueryTokenStream visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.UNBOUNDED())); - builder.append(QueryTokens.expression(ctx.PRECEDING())); + builder.append(QueryTokens.expression(ctx.CASE())); + + builder.append(QueryTokenStream.concat(ctx.caseWhenPredicateClause(), this::visit, TOKEN_SPACE)); + + if (ctx.ELSE() != null) { + + builder.append(QueryTokens.expression(ctx.ELSE())); + builder.appendExpression(visit(ctx.expressionOrPredicate())); + } + + builder.append(QueryTokens.expression(ctx.END())); return builder; } @Override - public QueryTokenStream visitExpressionPrecedingFrameStart(HqlParser.ExpressionPrecedingFrameStartContext ctx) { + public QueryTokenStream visitCaseWhenExpressionClause(HqlParser.CaseWhenExpressionClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.WHEN())); builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.PRECEDING())); + builder.append(QueryTokens.expression(ctx.THEN())); + builder.appendExpression(visit(ctx.expressionOrPredicate())); return builder; } @Override - public QueryTokenStream visitCurrentRowFrameStart(HqlParser.CurrentRowFrameStartContext ctx) { + public QueryTokenStream visitCaseWhenPredicateClause(HqlParser.CaseWhenPredicateClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); + builder.append(QueryTokens.expression(ctx.WHEN())); + builder.appendExpression(visit(ctx.predicate())); + builder.append(QueryTokens.expression(ctx.THEN())); + builder.appendExpression(visit(ctx.expressionOrPredicate())); return builder; } @Override - public QueryTokenStream visitExpressionFollowingFrameStart(HqlParser.ExpressionFollowingFrameStartContext ctx) { + public QueryTokenStream visitGenericFunction(HqlParser.GenericFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.FOLLOWING())); + nested.append(visit(ctx.genericFunctionName())); + nested.append(TOKEN_OPEN_PAREN); - return builder; - } + if (ctx.genericFunctionArguments() != null) { + nested.appendInline(visit(ctx.genericFunctionArguments())); + } else if (ctx.ASTERISK() != null) { + nested.append(QueryTokens.token(ctx.ASTERISK())); + } - @Override - public QueryTokenStream visitCurrentRowFrameExclusion(HqlParser.CurrentRowFrameExclusionContext ctx) { + nested.append(TOKEN_CLOSE_PAREN); + builder.append(nested); - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.pathContinutation() != null) { + builder.append(visit(ctx.pathContinutation())); + } - builder.append(QueryTokens.expression(ctx.EXCLUDE())); - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); + if (ctx.nthSideClause() != null) { + builder.appendExpression(visit(ctx.nthSideClause())); + } - return builder; - } + if (ctx.nullsClause() != null) { + builder.appendExpression(visit(ctx.nullsClause())); + } - @Override - public QueryTokenStream visitGroupFrameExclusion(HqlParser.GroupFrameExclusionContext ctx) { + if (ctx.withinGroupClause() != null) { + builder.appendExpression(visit(ctx.withinGroupClause())); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } - builder.append(QueryTokens.expression(ctx.EXCLUDE())); - builder.append(QueryTokens.expression(ctx.GROUP())); + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } return builder; } @Override - public QueryTokenStream visitTiesFrameExclusion(HqlParser.TiesFrameExclusionContext ctx) { + public QueryTokenStream visitFilterClause(HqlParser.FilterClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.EXCLUDE())); - builder.append(QueryTokens.expression(ctx.TIES())); + builder.append(QueryTokens.expression(ctx.FILTER())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.whereClause())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitNoOthersFrameExclusion(HqlParser.NoOthersFrameExclusionContext ctx) { + public QueryTokenStream visitOverClause(HqlParser.OverClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.OVER())); - builder.append(QueryTokens.expression(ctx.EXCLUDE())); - builder.append(QueryTokens.expression(ctx.NO())); - builder.append(QueryTokens.expression(ctx.OTHERS())); + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.append(TOKEN_OPEN_PAREN); - return builder; - } + List trees = new ArrayList<>(); - @Override - public QueryTokenStream visitExpressionPrecedingFrameEnd(HqlParser.ExpressionPrecedingFrameEndContext ctx) { + if (ctx.partitionClause() != null) { + trees.add(ctx.partitionClause()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.orderByClause() != null) { + trees.add(ctx.orderByClause()); + } - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.PRECEDING())); + if (ctx.frameClause() != null) { + trees.add(ctx.frameClause()); + } + + nested.appendInline(QueryTokenStream.concat(trees, this::visit, TOKEN_SPACE)); + nested.append(TOKEN_CLOSE_PAREN); + + builder.appendInline(nested); return builder; } @Override - public QueryTokenStream visitCurrentRowFrameEnd(HqlParser.CurrentRowFrameEndContext ctx) { + public QueryTokenStream visitPartitionClause(HqlParser.PartitionClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); + builder.append(QueryTokens.expression(ctx.PARTITION())); + builder.append(QueryTokens.expression(ctx.BY())); + + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitExpressionFollowingFrameEnd(HqlParser.ExpressionFollowingFrameEndContext ctx) { + public QueryTokenStream visitFrameClause(HqlParser.FrameClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.FOLLOWING())); + if (ctx.RANGE() != null) { + builder.append(QueryTokens.expression(ctx.RANGE())); + } else if (ctx.ROWS() != null) { + builder.append(QueryTokens.expression(ctx.ROWS())); + } else if (ctx.GROUPS() != null) { + builder.append(QueryTokens.expression(ctx.GROUPS())); + } - return builder; - } + if (ctx.BETWEEN() != null) { + builder.append(QueryTokens.expression(ctx.BETWEEN())); + } - @Override - public QueryTokenStream visitUnboundedFollowingFrameEnd(HqlParser.UnboundedFollowingFrameEndContext ctx) { + builder.appendExpression(visit(ctx.frameStart())); - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.AND() != null) { - builder.append(QueryTokens.expression(ctx.UNBOUNDED())); - builder.append(QueryTokens.expression(ctx.FOLLOWING())); + builder.append(QueryTokens.expression(ctx.AND())); + builder.appendExpression(visit(ctx.frameEnd())); + } + + if (ctx.frameExclusion() != null) { + builder.appendExpression(visit(ctx.frameExclusion())); + } return builder; } @@ -1940,23 +2730,49 @@ public QueryTokenStream visitExtractFunction(HqlParser.ExtractFunctionContext ct QueryRendererBuilder nested = QueryRenderer.builder(); - nested.appendExpression(visit(ctx.expression(0))); + nested.appendExpression(visit(ctx.extractField())); nested.append(QueryTokens.expression(ctx.FROM())); - nested.append(visit(ctx.expression(1))); + nested.append(visit(ctx.expression())); builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.dateTimeFunction() != null) { + } else if (ctx.datetimeField() != null) { - builder.append(visit(ctx.dateTimeFunction())); + builder.append(visit(ctx.datetimeField())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.expression(0))); + builder.appendInline(visit(ctx.expression())); builder.append(TOKEN_CLOSE_PAREN); } return builder; } + @Override + public QueryTokenStream visitExtractField(HqlParser.ExtractFieldContext ctx) { + + if (ctx.datetimeField() != null) { + return visit(ctx.datetimeField()); + } + + if (ctx.dayField() != null) { + return visit(ctx.dayField()); + } + + if (ctx.weekField() != null) { + return visit(ctx.weekField()); + } + + if (ctx.timeZoneField() != null) { + return visit(ctx.timeZoneField()); + } + + if (ctx.dateOrTimeField() != null) { + return visit(ctx.dateOrTimeField()); + } + + return QueryRenderer.builder(); + } + @Override public QueryTokenStream visitTrimFunction(HqlParser.TrimFunctionContext ctx) { @@ -1965,31 +2781,51 @@ public QueryTokenStream visitTrimFunction(HqlParser.TrimFunctionContext ctx) { builder.append(QueryTokens.token(ctx.TRIM())); builder.append(TOKEN_OPEN_PAREN); - if (ctx.LEADING() != null) { - builder.append(QueryTokens.expression(ctx.LEADING())); - } else if (ctx.TRAILING() != null) { - builder.append(QueryTokens.expression(ctx.TRAILING())); - } else if (ctx.BOTH() != null) { - builder.append(QueryTokens.expression(ctx.BOTH())); + if (ctx.trimSpecification() != null) { + builder.appendExpression(visit(ctx.trimSpecification())); } - if (ctx.stringLiteral() != null) { - builder.append(visit(ctx.stringLiteral())); + if (ctx.trimCharacter() != null) { + builder.appendExpression(visit(ctx.trimCharacter())); } if (ctx.FROM() != null) { builder.append(QueryTokens.expression(ctx.FROM())); } - builder.appendInline(visit(ctx.expression())); + if (ctx.expression() != null) { + builder.append(visit(ctx.expression())); + } + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitDateTimeFunction(HqlParser.DateTimeFunctionContext ctx) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.d)); + public QueryTokenStream visitTrimSpecification(HqlParser.TrimSpecificationContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.BOTH() != null) { + builder.append(QueryTokens.expression(ctx.BOTH())); + } else if (ctx.LEADING() != null) { + builder.append(QueryTokens.expression(ctx.LEADING())); + } else if (ctx.TRAILING() != null) { + builder.append(QueryTokens.expression(ctx.TRAILING())); + } + + return builder.build(); + } + + @Override + public QueryTokenStream visitTrimCharacter(HqlParser.TrimCharacterContext ctx) { + + if (ctx.stringLiteral() != null) { + return visit(ctx.stringLiteral()); + } + + return visit(ctx.parameter()); } @Override @@ -1997,25 +2833,32 @@ public QueryTokenStream visitEveryFunction(HqlParser.EveryFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.every)); + builder.appendExpression(visit(ctx.everyAllQuantifier())); - if (ctx.ELEMENTS() != null) { - builder.append(QueryTokens.expression(ctx.ELEMENTS())); - } else if (ctx.INDICES() != null) { - builder.append(QueryTokens.expression(ctx.INDICES())); - } + if (ctx.predicate() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.predicate())); + builder.append(TOKEN_CLOSE_PAREN); - builder.append(TOKEN_OPEN_PAREN); + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } - if (ctx.predicate() != null) { - builder.append(visit(ctx.predicate())); + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } } else if (ctx.subquery() != null) { - builder.append(visit(ctx.subquery())); - } else if (ctx.simplePath() != null) { - builder.append(visit(ctx.simplePath())); - } + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.subquery())); + builder.append(TOKEN_CLOSE_PAREN); + } else { - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.collectionQuantifier())); + + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.simplePath())); + builder.append(TOKEN_CLOSE_PAREN); + } return builder; } @@ -2025,25 +2868,32 @@ public QueryTokenStream visitAnyFunction(HqlParser.AnyFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.any)); + builder.appendExpression(visit(ctx.anySomeQuantifier())); - if (ctx.ELEMENTS() != null) { - builder.append(QueryTokens.expression(ctx.ELEMENTS())); - } else if (ctx.INDICES() != null) { - builder.append(QueryTokens.expression(ctx.INDICES())); - } + if (ctx.predicate() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.predicate())); + builder.append(TOKEN_CLOSE_PAREN); - builder.append(TOKEN_OPEN_PAREN); + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } - if (ctx.predicate() != null) { - builder.append(visit(ctx.predicate())); + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } } else if (ctx.subquery() != null) { - builder.append(visit(ctx.subquery())); - } else if (ctx.simplePath() != null) { - builder.append(visit(ctx.simplePath())); - } + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.subquery())); + builder.append(TOKEN_CLOSE_PAREN); + } else { - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.collectionQuantifier())); + + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.simplePath())); + builder.append(TOKEN_CLOSE_PAREN); + } return builder; } @@ -2528,16 +3378,6 @@ public QueryTokenStream visitIdentifier(HqlParser.IdentifierContext ctx) { } } - @Override - public QueryTokenStream visitCharacter(HqlParser.CharacterContext ctx) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); - } - - @Override - public QueryTokenStream visitFunctionName(HqlParser.FunctionNameContext ctx) { - return QueryTokenStream.concat(ctx.reservedWord(), this::visit, TOKEN_DOT); - } - @Override public QueryTokenStream visitReservedWord(HqlParser.ReservedWordContext ctx) { 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 8a23e279cd..4c25ec66f0 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,6 +37,7 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch * @since 3.1 */ class HqlQueryRendererTests { @@ -1551,9 +1552,11 @@ 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"); + @ParameterizedTest // GH-3025 + @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", + "NANOSECOND", "EPOCH" }) + void durationLiteralsShouldWork(String dtField) { + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE (ce.endDate - ce.startDate) > 5 %s".formatted(dtField)); } @Test // GH-3025 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 3e459fa26e..0dff2c328e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -21,6 +21,9 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -31,6 +34,7 @@ * IMPORTANT: Purely verifies the parser without any transformations. * * @author Greg Turnquist + * @author Mark Paluch * @since 3.1 */ class HqlSpecificationTests { @@ -331,6 +335,177 @@ OR TREAT(e AS Contractor).hours > 100 """); } + @Test // GH-3689 + void generic() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE FOO(x).bar RESPECT NULLS + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE FOO(x).bar IGNORE NULLS + """); + } + + @Test // GH-3689 + void size() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE SIZE(x) > 1 + """); + } + + @Test // GH-3689 + void collectionAggregate() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE MAXELEMENT(foo) > MINELEMENT(bar) + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE MININDEX(foo) > MAXINDEX(bar) + """); + } + + @Test // GH-3689 + void trunc() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(x) = TRUNCATE(y) + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, 'foo') = TRUNCATE(e, 'bar') + """); + } + + @ParameterizedTest // GH-3689 + @ValueSource(strings = { "YYYY", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", + "NANOSECOND", "EPOCH" }) + void trunc(String truncation) { + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, %1$s) = TRUNCATE(e, %1$s) + """.formatted(truncation)); + } + + @Test // GH-3689 + void format() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE FORMAT(x AS 'foo') = FORMAT(x AS 'bar') + """); + } + + @Test // GH-3689 + void collate() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE COLLATE(x AS foo) = COLLATE(x AS foo.bar) + """); + } + + @Test // GH-3689 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1 FOR 2) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1) " + // + "from Call c"); + } + + @Test // GH-3689 + void overlay() { + + assertQuery("select OVERLAY(c.number PLACING 1 FROM 2) " + // + "from Call c "); + + assertQuery("select OVERLAY(p.number PLACING 1 FROM 2 FOR 3) " + // + "from Call c "); + } + + @Test // GH-3689 + void pad() { + + assertQuery("select PAD(c.number WITH 1 LEADING) " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 TRAILING) " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 LEADING '0') " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 TRAILING '0') " + // + "from Call c "); + } + + @Test // GH-3689 + void position() { + + assertQuery("select POSITION(c.number IN 'foo') " + // + "from Call c "); + } + + @Test // GH-3689 + void currentDateFunctions() { + + assertQuery("select CURRENT DATE, CURRENT_DATE() " + // + "from Call c "); + + assertQuery("select CURRENT TIME, CURRENT_TIME() " + // + "from Call c "); + + assertQuery("select CURRENT TIMESTAMP, CURRENT_TIMESTAMP() " + // + "from Call c "); + + assertQuery("select INSTANT, CURRENT_INSTANT() " + // + "from Call c "); + + assertQuery("select LOCAL DATE, LOCAL_DATE() " + // + "from Call c "); + + assertQuery("select LOCAL TIME, LOCAL_TIME() " + // + "from Call c "); + + assertQuery("select LOCAL DATETIME, LOCAL_DATETIME() " + // + "from Call c "); + + assertQuery("select OFFSET DATETIME, OFFSET_DATETIME() " + // + "from Call c "); + } + + @Test // GH-3689 + void cube() { + + assertQuery("select CUBE(foo), CUBE(foo, bar) " + // + "from Call c "); + } + + @Test // GH-3689 + void rollup() { + + assertQuery("select ROLLUP(foo), ROLLUP(foo, bar) " + // + "from Call c "); + } + @Test void pathExpressionsNamedParametersExample() { @@ -383,6 +558,80 @@ WHERE EXISTS (SELECT spouseEmp """); } + @Test // GH-3689 + void everyAll() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EVERY (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL (foo > 1) OVER (PARTITION BY bar) + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL VALUES (foo) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL ELEMENTS (foo) > 1 + """); + } + + @Test // GH-3689 + void anySome() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE SOME (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY (foo > 1) OVER (PARTITION BY bar) + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY VALUES (foo) > 1 + """); + } + + @Test // GH-3689 + void listAgg() { + + assertQuery("select listagg(p.number, ', ') within group (order by p.type, p.number) " + // + "from Phone p " + // + "group by p.person"); + } + @Test void allExample() { @@ -1119,9 +1368,6 @@ void hqlQueries() { assertQuery("select concat(p.number, ' : ', cast(c.duration as string)) " + // "from Call c " + // "join c.phone p"); - assertQuery("select substring(p.number, 1, 2) " + // - "from Call c " + // - "join c.phone p"); assertQuery("select upper(p.name) " + // "from Person p "); assertQuery("select lower(p.name) " + // @@ -1450,9 +1696,6 @@ void hqlQueries() { "from Call c " + // "join c.phone p " + // "group by p.number"); - assertQuery("select listagg(p.number, ', ') within group (order by p.type, p.number) " + // - "from Phone p " + // - "group by p.person"); assertQuery("select sum(c.duration) " + // "from Call c "); assertQuery("select p.name, sum(c.duration) " + // From f3af709d18d911f1af609afc254f8d5f5724decf Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 27 Nov 2024 15:16:00 +0100 Subject: [PATCH 4/4] Add HQL rendering tests. Extend tests for parsing built in hql functions and expressions. Assert count query creation works fine for when nested function calls are present in select. Fix minor rendering issue that added superfluous leading whitespace to expressions nested within a function. --- .../jpa/repository/query/QueryRenderer.java | 3 +- .../query/HqlQueryRendererTests.java | 3 + .../query/HqlQueryTransformerTests.java | 3 +- .../query/HqlSpecificationTests.java | 105 +++++++++++++----- 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java index 1bdde97beb..87fe53e050 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java @@ -38,6 +38,7 @@ * * * @author Mark Paluch + * @author Christoph Strobl */ abstract class QueryRenderer implements QueryTokenStream { @@ -243,7 +244,7 @@ String render() { for (QueryRenderer queryRenderer : nested) { if (lastAppended != null && (lastExpression || queryRenderer.isExpression()) && !builder.isEmpty() - && !lastAppended.endsWith(" ")) { + && (!lastAppended.endsWith(" ") && !lastAppended.endsWith("("))) { builder.append(' '); } 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 4c25ec66f0..99547994e1 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 @@ -1556,7 +1556,10 @@ void castFunctionWithFqdnShouldWork() { @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", "NANOSECOND", "EPOCH" }) 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 as id, cd.startDate + 5 %s AS summedDate FROM CalendarEvent ce".formatted(dtField)); } @Test // GH-3025 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 482b02db47..40aa7d274b 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 @@ -1090,7 +1090,7 @@ void aliasesShouldNotOverlapWithSortProperties() { "SELECT t3 FROM Test3 t3 JOIN t3.test2 x WHERE x.id = :test2Id order by t3.testDuplicateColumnName desc"); } - @Test // GH-3269 + @Test // GH-3269, GH-3689 void createsCountQueryUsingAliasCorrectly() { assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee AS __"); @@ -1102,6 +1102,7 @@ void createsCountQueryUsingAliasCorrectly() { "select count(distinct a, b, sum(amount), d) from Employee AS __ GROUP BY n"); assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", "select count(distinct a, count(b)) from Employee AS __ GROUP BY n"); + assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); } @Test // GH-3427 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 0dff2c328e..80483a05b9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; - import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -35,6 +34,7 @@ * * @author Greg Turnquist * @author Mark Paluch + * @author Christoph Strobl * @since 3.1 */ class HqlSpecificationTests { @@ -335,18 +335,15 @@ OR TREAT(e AS Contractor).hours > 100 """); } - @Test // GH-3689 - void generic() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE FOO(x).bar RESPECT NULLS - """); + @ParameterizedTest // GH-3689 + @ValueSource(strings = { "RESPECT NULLS", "IGNORE NULLS" }) + void generic(String nullHandling) { + // not in the official documentation but supported in the grammar. assertQuery(""" SELECT e FROM Employee e - WHERE FOO(x).bar IGNORE NULLS - """); + WHERE FOO(x).bar %s + """.formatted(nullHandling)); } @Test // GH-3689 @@ -356,6 +353,11 @@ void size() { SELECT e FROM Employee e WHERE SIZE(x) > 1 """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE SIZE(e.skills) > 1 + """); } @Test // GH-3689 @@ -384,10 +386,15 @@ WHERE TRUNC(x) = TRUNCATE(y) SELECT e FROM Employee e WHERE TRUNC(e, 'foo') = TRUNCATE(e, 'bar') """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, 'YEAR') = TRUNCATE(LOCAL DATETIME, 'YEAR') + """); } @ParameterizedTest // GH-3689 - @ValueSource(strings = { "YYYY", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", + @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", "NANOSECOND", "EPOCH" }) void trunc(String truncation) { @@ -402,7 +409,17 @@ void format() { assertQuery(""" SELECT e FROM Employee e - WHERE FORMAT(x AS 'foo') = FORMAT(x AS 'bar') + WHERE FORMAT(x AS 'yyyy') = FORMAT(e.hiringDate AS 'yyyy') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL DATETIME as 'yyyy-MM-dd') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL_DATE() as 'yyyy-MM-dd') """); } @@ -411,7 +428,7 @@ void collate() { assertQuery(""" SELECT e FROM Employee e - WHERE COLLATE(x AS foo) = COLLATE(x AS foo.bar) + WHERE COLLATE(x AS ucs_basic) = COLLATE(e.name AS ucs_basic) """); } @@ -424,11 +441,20 @@ void substring() { assertQuery("select substring(c.number, 1) " + // "from Call c"); + assertQuery("select substring(c.number, 1, position('/0' in c.number)) " + // + "from Call c"); + assertQuery("select substring(c.number FROM 1 FOR 2) " + // "from Call c"); assertQuery("select substring(c.number FROM 1) " + // "from Call c"); + + assertQuery("select substring(c.number FROM 1 FOR position('/0' in c.number)) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1) AS shortNumber " + // + "from Call c"); } @Test // GH-3689 @@ -462,6 +488,9 @@ void position() { assertQuery("select POSITION(c.number IN 'foo') " + // "from Call c "); + + assertQuery("select POSITION(c.number IN 'foo') + 1 AS pos " + // + "from Call c "); } @Test // GH-3689 @@ -490,6 +519,9 @@ void currentDateFunctions() { assertQuery("select OFFSET DATETIME, OFFSET_DATETIME() " + // "from Call c "); + + assertQuery("select OFFSET DATETIME AS offsetDatetime, OFFSET_DATETIME() AS offset_datetime " + // + "from Call c "); } @Test // GH-3689 @@ -497,6 +529,8 @@ void cube() { assertQuery("select CUBE(foo), CUBE(foo, bar) " + // "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY CUBE(state, province)"); } @Test // GH-3689 @@ -504,6 +538,8 @@ void rollup() { assertQuery("select ROLLUP(foo), ROLLUP(foo, bar) " + // "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY ROLLUP(state, province)"); } @Test @@ -710,16 +746,13 @@ WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE """); } - @Test // GH-3628 - void functionInvocationWithIsBoolean() { - - assertQuery(""" - from RoleTmpl where find_in_set(:appId, appIds) is true - """); + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "is true", "is not true", "is false", "is not false" }) + void functionInvocationWithIsBoolean(String booleanComparison) { assertQuery(""" - from RoleTmpl where find_in_set(:appId, appIds) is false - """); + from RoleTmpl where find_in_set(:appId, appIds) %s + """.formatted(booleanComparison)); } @Test @@ -1094,20 +1127,36 @@ void booleanPredicate() { """); } - @Test // GH-3628 - void distinctFromPredicate() { + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "IS DISTINCT FROM", "IS NOT DISTINCT FROM" }) + void distinctFromPredicate(String distinctFrom) { assertQuery(""" SELECT c FROM Customer c - WHERE c.orders IS DISTINCT FROM c.payments - """); + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); assertQuery(""" SELECT c FROM Customer c - WHERE c.orders IS NOT DISTINCT FROM c.payments - """); + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + GROUP BY c.lastname + HAVING c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + WHERE EXISTS (SELECT c2 + FROM Customer c2 + WHERE c2.orders %s c.orders) + """.formatted(distinctFrom)); } @Test @@ -1576,7 +1625,7 @@ void hqlQueries() { assertQuery("select longest.duration " + // "from Phone p " + // "left join lateral (" + // - " select c.duration as duration " + // + "select c.duration as duration " + // " from p.calls c" + // " order by c.duration desc" + // " limit 1 " + //