Add support for JPA 3.2 additions to EQL.

See: #3136
Original Pull Request: #3695
This commit is contained in:
Christoph Strobl
2024-06-21 18:14:07 +02:00
committed by Mark Paluch
parent ebda35d385
commit 262e05eda8
9 changed files with 345 additions and 18 deletions

View File

@@ -309,6 +309,7 @@ scalar_expression
| datetime_expression
| boolean_expression
| case_expression
| cast_function
| entity_type_expression
;
@@ -458,6 +459,7 @@ string_expression
| string_cast_function
| type_cast_function
| '(' subquery ')'
| string_expression '||' string_expression
;
datetime_expression
@@ -542,6 +544,9 @@ functions_returning_strings
| TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')'
| LOWER '(' string_expression ')'
| UPPER '(' string_expression ')'
| REPLACE '(' string_expression ',' string_expression ',' string_expression ')'
| LEFT '(' string_expression ',' arithmetic_expression ')'
| RIGHT '(' string_expression ',' arithmetic_expression ')'
;
trim_specification
@@ -625,6 +630,14 @@ nullif_expression
: NULLIF '(' scalar_expression ',' scalar_expression ')'
;
type_literal
: STRING
| INTEGER
| LONG
| FLOAT
| DOUBLE
;
/*******************
Gaps in the spec.
*******************/
@@ -637,6 +650,7 @@ trim_character
identification_variable
: IDENTIFICATION_VARIABLE
| f=(COUNT
| AS
| DATE
| FROM
| INNER
@@ -646,11 +660,13 @@ identification_variable
| ORDER
| OUTER
| POWER
| RIGHT
| FLOOR
| SIGN
| TIME
| TYPE
| VALUE)
| type_literal
;
constructor_name
@@ -832,6 +848,8 @@ reserved_word
|OR
|ORDER
|OUTER
|REPLACE
|RIGHT
|POWER
|ROUND
|SELECT
@@ -928,6 +946,7 @@ EXTRACT : E X T R A C T;
FALSE : F A L S E;
FETCH : F E T C H;
FIRST : F I R S T;
FLOAT : F L O A T;
FLOOR : F L O O R;
FLOAT : F L O A T;
FROM : F R O M;
@@ -937,6 +956,7 @@ HAVING : H A V I N G;
IN : I N;
INDEX : I N D E X;
INNER : I N N E R;
INTEGER : I N T E G E R;
INTERSECT : I N T E R S E C T;
IS : I S;
INTEGER : I N T E G E R;
@@ -969,6 +989,8 @@ ORDER : O R D E R;
OUTER : O U T E R;
POWER : P O W E R;
REGEXP : R E G E X P;
REPLACE : R E P L A C E;
RIGHT : R I G H T;
ROUND : R O U N D;
SELECT : S E L E C T;
SET : S E T;
@@ -976,6 +998,7 @@ SIGN : S I G N;
SIZE : S I Z E;
SOME : S O M E;
SQRT : S Q R T;
STRING : S T R I N G;
SUBSTRING : S U B S T R I N G;
STRING : S T R I N G;
SUM : S U M;
@@ -996,7 +1019,6 @@ WHERE : W H E R E;
EQUAL : '=' ;
NOT_EQUAL : '<>' | '!=' ;
CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ;
IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ;
STRINGLITERAL : '\'' (~ ('\'' | '\\')|'\\')* '\'' ;

View File

@@ -43,13 +43,25 @@ ql_statement
;
select_statement
: select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator_with_select_statement)*
: select_query
;
setOperator_with_select_statement
: INTERSECT select_statement
| UNION select_statement
| EXCEPT select_statement
select_query
: select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)?
;
setOperator
: UNION ALL?
| INTERSECT ALL?
| EXCEPT ALL?
;
set_fuction
: setOperator set_function_select
;
set_function_select
: select_query
;
update_statement
@@ -303,6 +315,7 @@ scalar_expression
| datetime_expression
| boolean_expression
| case_expression
| cast_expression
| entity_type_expression
;
@@ -453,6 +466,7 @@ string_expression
| string_cast_function
| type_cast_function
| '(' subquery ')'
| string_expression '||' string_expression
;
datetime_expression
@@ -536,7 +550,10 @@ functions_returning_strings
| SUBSTRING '(' string_expression ',' arithmetic_expression (',' arithmetic_expression)? ')'
| TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')'
| LOWER '(' string_expression ')'
| REPLACE '(' string_expression ',' string_expression ',' string_expression ')'
| UPPER '(' string_expression ')'
| LEFT '(' string_expression ',' arithmetic_expression ')'
| RIGHT '(' string_expression ',' arithmetic_expression ')'
;
trim_specification
@@ -620,6 +637,10 @@ nullif_expression
: NULLIF '(' scalar_expression ',' scalar_expression ')'
;
cast_expression
: CAST '(' string_expression AS type_literal ')'
;
/*******************
Gaps in the spec.
*******************/
@@ -641,6 +662,7 @@ identification_variable
| ORDER
| OUTER
| POWER
| RIGHT
| FLOOR
| SIGN
| TIME
@@ -691,6 +713,14 @@ numeric_literal
| LONGLITERAL
;
type_literal
: STRING
| INTEGER
| LONG
| FLOAT
| DOUBLE
;
boolean_literal
: TRUE
| FALSE
@@ -827,6 +857,8 @@ reserved_word
|ORDER
|OUTER
|POWER
|REPLACE
|RIGHT
|ROUND
|SELECT
|SET
@@ -922,6 +954,7 @@ EXTRACT : E X T R A C T;
FALSE : F A L S E;
FETCH : F E T C H;
FIRST : F I R S T;
FLOAT : F L O A T;
FLOOR : F L O O R;
FLOAT : F L O A T;
FROM : F R O M;
@@ -931,6 +964,7 @@ HAVING : H A V I N G;
IN : I N;
INDEX : I N D E X;
INNER : I N N E R;
INTEGER : I N T E G E R;
INTERSECT : I N T E R S E C T;
IS : I S;
INTEGER : I N T E G E R;
@@ -961,6 +995,8 @@ ON : O N;
OR : O R;
ORDER : O R D E R;
OUTER : O U T E R;
REPLACE : R E P L A C E;
RIGHT : R I G H T;
POWER : P O W E R;
REGEXP : R E G E X P;
ROUND : R O U N D;
@@ -970,6 +1006,7 @@ SIGN : S I G N;
SIZE : S I Z E;
SOME : S O M E;
SQRT : S Q R T;
STRING : S T R I N G;
SUBSTRING : S U B S T R I N G;
STRING : S T R I N G;
SUM : S U M;

View File

@@ -24,6 +24,7 @@ import org.antlr.v4.runtime.tree.ParseTree;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes.
@@ -1009,6 +1010,8 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
builder.append(visit(ctx.case_expression()));
} else if (ctx.entity_type_expression() != null) {
builder.append(visit(ctx.entity_type_expression()));
} else if (ctx.cast_function() != null) {
return (visit(ctx.cast_function()));
}
return builder;
@@ -1610,6 +1613,11 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.subquery()));
builder.append(TOKEN_CLOSE_PAREN);
} else if (!ObjectUtils.isEmpty(ctx.string_expression())) {
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_DOUBLE_PIPE);
builder.appendExpression(visit(ctx.string_expression(1)));
}
return builder;
@@ -1941,6 +1949,32 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.LEFT() != null) {
builder.append(QueryTokens.token(ctx.LEFT()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.arithmetic_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.RIGHT() != null) {
builder.append(QueryTokens.token(ctx.RIGHT()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.arithmetic_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.REPLACE() != null) {
builder.append(QueryTokens.token(ctx.REPLACE()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.string_expression(1)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.string_expression(2)));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
@@ -1986,7 +2020,7 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
if (ctx.AS() != null) {
builder.append(QueryTokens.expression(ctx.AS()));
}
builder.appendInline(visit(ctx.identification_variable()));
builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE));
if (!CollectionUtils.isEmpty(ctx.numeric_literal())) {
@@ -2106,6 +2140,14 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
}
}
@Override
public QueryRendererBuilder visitType_literal(EqlParser.Type_literalContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText())));
return builder;
}
@Override
public QueryTokenStream visitGeneral_case_expression(EqlParser.General_case_expressionContext ctx) {
@@ -2226,9 +2268,11 @@ class EqlQueryRenderer extends EqlBaseVisitor<QueryTokenStream> {
return QueryRendererBuilder.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE()));
} else if (ctx.f != null) {
return QueryRendererBuilder.from(QueryTokens.expression(ctx.f));
} else {
return QueryRenderer.builder();
} else if (ctx.type_literal() != null) {
return visit(ctx.type_literal());
}
return QueryRenderer.builder();
}
@Override

View File

@@ -42,7 +42,17 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer {
}
@Override
public QueryRenderer.QueryRendererBuilder visitSelect_statement(JpqlParser.Select_statementContext ctx) {
public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) {
if(ctx.select_query() != null) {
return visitSelect_query(ctx.select_query());
}
return QueryTokenStream.empty();
}
@Override
public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();

View File

@@ -15,17 +15,34 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COLON;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOT;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_EQUALS;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_QUESTION_MARK;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOUBLE_PIPE;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_SPACE;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN;
import java.util.ArrayList;
import java.util.List;
import org.antlr.v4.runtime.tree.ParseTree;
import org.springframework.data.jpa.repository.query.JpqlParser.Except_clauseContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Intersect_clauseContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Relation_fuctions_selectContext;
import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Cast_expressionContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Type_literalContext;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes.
@@ -56,8 +73,17 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
}
}
@Override
public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) {
@Override
public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) {
if(ctx.select_query() != null) {
return visitSelect_query(ctx.select_query());
}
return QueryTokenStream.empty();
}
public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
@@ -80,11 +106,11 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
builder.appendExpression(visit(ctx.orderby_clause()));
}
ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> {
tokens.addAll(visit(setOperatorWithSelectStatementContext));
});
if(ctx.set_fuction() != null) {
builder.appendExpression(visit(ctx.set_fuction()));
}
return tokens;
return builder;
}
@Override
@@ -800,6 +826,19 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
return builder;
}
@Override
public QueryTokenStream visitSet_fuction(Set_fuctionContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(QueryTokens.expression(ctx.setOperator().getStart()));
if(ctx.setOperator().ALL() != null) {
builder.append(QueryTokens.expression(ctx.setOperator().ALL()));
}
builder.appendExpression(visit(ctx.set_function_select().select_query()));
return builder;
}
@Override
public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) {
@@ -974,6 +1013,8 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
return visit(ctx.case_expression());
} else if (ctx.entity_type_expression() != null) {
return visit(ctx.entity_type_expression());
} else if (ctx.cast_expression() != null) {
return (visit(ctx.cast_expression()));
}
return QueryTokenStream.empty();
@@ -1570,6 +1611,11 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.subquery()));
builder.append(TOKEN_CLOSE_PAREN);
} else if (!ObjectUtils.isEmpty(ctx.string_expression())) {
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_DOUBLE_PIPE);
builder.appendExpression(visit(ctx.string_expression(1)));
}
return builder;
@@ -1890,6 +1936,29 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
builder.append(TOKEN_OPEN_PAREN);
builder.append(visit(ctx.string_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.LEFT() != null) {
builder.append(QueryTokens.token(ctx.LEFT()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.arithmetic_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.RIGHT() != null) {
builder.append(QueryTokens.token(ctx.RIGHT()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.arithmetic_expression(0)));
builder.append(TOKEN_CLOSE_PAREN);
} else if (ctx.REPLACE() != null) {
builder.append(QueryTokens.token(ctx.REPLACE()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression(0)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.string_expression(1)));
builder.append(TOKEN_COMMA);
builder.appendInline(visit(ctx.string_expression(2)));
builder.append(TOKEN_CLOSE_PAREN);
}
return builder;
@@ -2048,6 +2117,26 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
}
}
@Override
public QueryRendererBuilder visitCast_expression(Cast_expressionContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(QueryTokens.token(ctx.CAST()));
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.string_expression()));
builder.append(QueryTokens.expression(ctx.AS()));
builder.appendInline(visit(ctx.type_literal()));
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
@Override
public QueryRendererBuilder visitType_literal(Type_literalContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText())));
return builder;
}
@Override
public QueryTokenStream visitGeneral_case_expression(JpqlParser.General_case_expressionContext ctx) {

View File

@@ -54,6 +54,16 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
@Override
public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) {
if(ctx.select_query() != null) {
return visitSelect_query(ctx.select_query());
}
return QueryTokenStream.empty();
}
@Override
public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.select_clause()));
@@ -96,7 +106,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
return builder.append(dtoDelegate.transformSelectionList(tokenStream));
}
private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) {
private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_queryContext ctx) {
if (ctx.orderby_clause() != null) {
QueryTokenStream existingOrder = visit(ctx.orderby_clause());

View File

@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
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;
@@ -412,4 +414,53 @@ class EqlComplianceTests {
assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)");
assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)");
}
@Test // GH-3496
void lateralShouldBeAValidParameter() {
assertQuery("select e from Employee e where e.lateral = :_lateral");
assertQuery("select te from TestEntity te where te.lateral = :lateral");
}
@Test // GH-3136
void intersect() {
assertQuery("""
SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1
INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2
""");
}
@Test // GH-3136
void except() {
assertQuery("""
SELECT e FROM Employee e
EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary
""");
}
@ParameterizedTest // GH-3136
@ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"})
void jpqlCast(String targetType) {
assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType));
}
@ParameterizedTest // GH-3136
@ValueSource(strings = {"LEFT", "RIGHT"})
void leftRightStringFunctions(String keyword) {
assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword));
}
@Test // GH-3136
void replaceStringFunctions() {
assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e");
assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e");
}
@Test // GH-3136
void stringConcatWithPipes() {
assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e");
}
}

View File

@@ -1983,6 +1983,18 @@ class HqlQueryRendererTests {
assertQuery(source);
}
@ParameterizedTest // GH-3136
@ValueSource(strings = {"LEFT", "RIGHT"})
void leftRightStringFunctions(String keyword) {
assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword));
}
@Test // GH-3136
void replaceStringFunctions() {
assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e");
assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e");
}
@Test
void reservedWordsShouldWork() {

View File

@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
/**
* Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in
@@ -63,4 +65,54 @@ class JpqlComplianceTests {
assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se");
}
@Test // GH-3136
void union() {
assertQuery("""
SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1
UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2
""");
}
@Test // GH-3136
void intersect() {
assertQuery("""
SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1
INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2
""");
}
@Test // GH-3136
void except() {
assertQuery("""
SELECT e FROM Employee e
EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary
""");
}
@ParameterizedTest // GH-3136
@ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"})
void cast(String targetType) {
assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType));
}
@ParameterizedTest // GH-3136
@ValueSource(strings = {"LEFT", "RIGHT"})
void leftRightStringFunctions(String keyword) {
assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword));
}
@Test // GH-3136
void replaceStringFunctions() {
assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e");
assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e");
}
@Test // GH-3136
void stringConcatWithPipes() {
assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e");
}
}