-
Notifications
You must be signed in to change notification settings - Fork 80
Support for Function Expressions in JDQL and Fluent API (#643) #704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
77fe165
3201e14
f496449
bb70dc4
6a73ec4
c61f3d8
68916e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /* | ||
| * Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| * All rights reserved. This program and the accompanying materials | ||
| * are made available under the terms of the Eclipse Public License v1.0 | ||
| * and Apache License v2.0 which accompanies this distribution. | ||
| * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | ||
| * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. | ||
| * You may elect to redistribute this code under either of these licenses. | ||
| * Contributors: | ||
| * Matheus Oliveira | ||
| */ | ||
| package org.eclipse.jnosql.communication.query; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * A query value that represents a function. | ||
| */ | ||
| public record FunctionQueryValue(Function function) implements QueryValue<Function> { | ||
|
|
||
| public FunctionQueryValue { | ||
| Objects.requireNonNull(function, "function is required"); | ||
| } | ||
|
|
||
| @Override | ||
| public Function get() { | ||
| return function; | ||
| } | ||
|
|
||
| @Override | ||
| public ValueType type() { | ||
| return ValueType.FUNCTION; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return function.toString(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* | ||
| * Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| * All rights reserved. This program and the accompanying materials | ||
| * are made available under the terms of the Eclipse Public License v1.0 | ||
| * and Apache License v2.0 which accompanies this distribution. | ||
| * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | ||
| * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. | ||
| * You may elect to redistribute this code under either of these licenses. | ||
| * Contributors: | ||
| * Matheus Oliveira | ||
| */ | ||
| package org.eclipse.jnosql.communication.query.data; | ||
|
|
||
| import org.eclipse.jnosql.communication.query.Function; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * The default implementation of {@link Function} | ||
| * @param name the function name | ||
| * @param params the parameters | ||
| */ | ||
| public record DefaultFunction(String name, Object... params) implements Function { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this record as public? Maybe default package is just enought |
||
|
|
||
| public DefaultFunction { | ||
| Objects.requireNonNull(name, "name is required"); | ||
| Objects.requireNonNull(params, "params is required"); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return name + "(" + Arrays.toString(params) + ")"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,14 @@ | |
| * You may elect to redistribute this code under either of these licenses. | ||
| * Contributors: | ||
| * Otavio Santana | ||
| * Matheus Oliveira | ||
| */ | ||
| package org.eclipse.jnosql.communication.query.data; | ||
|
|
||
| import org.eclipse.jnosql.communication.QueryException; | ||
| import org.eclipse.jnosql.communication.query.BooleanQueryValue; | ||
| import org.eclipse.jnosql.communication.query.EnumQueryValue; | ||
| import org.eclipse.jnosql.communication.query.FunctionQueryValue; | ||
| import org.eclipse.jnosql.communication.query.NumberQueryValue; | ||
| import org.eclipse.jnosql.communication.query.QueryPath; | ||
| import org.eclipse.jnosql.communication.query.QueryValue; | ||
|
|
@@ -53,10 +55,17 @@ public QueryValue<?> apply(JDQLParser.Primary_expressionContext context) { | |
| default -> | ||
| throw new UnsupportedOperationException("The special expression is not supported yet: " + specialExpression); | ||
| }; | ||
| } else if(context.enum_literal() != null) { | ||
| } else if (context.function_expression() != null) { | ||
| var functionExpression = context.function_expression(); | ||
| var functionName = getFunctionName(functionExpression); | ||
| var params = functionExpression.scalar_expression().stream() | ||
| .map(this::processScalar) | ||
| .toArray(); | ||
| return new FunctionQueryValue(new DefaultFunction(functionName, params)); | ||
| } else if (context.enum_literal() != null) { | ||
| Enum<?> value = EnumConverter.INSTANCE.apply(context.enum_literal().getText()); | ||
| return EnumQueryValue.of(value); | ||
| } else if(context.state_field_path_expression() != null) { | ||
| } else if (context.state_field_path_expression() != null) { | ||
| var stateContext = context.state_field_path_expression(); | ||
| var stateContextText = stateContext.getText(); | ||
| try { | ||
|
|
@@ -67,6 +76,33 @@ public QueryValue<?> apply(JDQLParser.Primary_expressionContext context) { | |
| return QueryPath.of(stateContextText); | ||
| } | ||
| } | ||
| throw new UnsupportedOperationException("The primary expression is not supported yet: " + context.getText()); | ||
| throw new UnsupportedOperationException("The primary expression is not supported yet: " + context.getText()); | ||
| } | ||
| } | ||
|
|
||
| private Object processScalar(JDQLParser.Scalar_expressionContext ctx) { | ||
| if (ctx.primary_expression() != null) { | ||
| return apply(ctx.primary_expression()); | ||
| } | ||
| if (ctx.LPAREN() != null && ctx.scalar_expression().size() == 1) { | ||
| return processScalar(ctx.scalar_expression(0)); | ||
| } | ||
| return ctx.getText(); | ||
| } | ||
|
|
||
| private String getFunctionName(JDQLParser.Function_expressionContext ctx) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The String is ok, but given that this is the implementation, we can define another strategy, such as an enum or even a contract class that extends Function. |
||
| if (ctx.ABS() != null) { | ||
| return "ABS"; | ||
| } else if (ctx.LENGTH() != null) { | ||
| return "LENGTH"; | ||
| } else if (ctx.LOWER() != null) { | ||
| return "LOWER"; | ||
| } else if (ctx.UPPER() != null) { | ||
| return "UPPER"; | ||
| } else if (ctx.LEFT() != null) { | ||
| return "LEFT"; | ||
| } else if (ctx.RIGHT() != null) { | ||
| return "RIGHT"; | ||
| } | ||
| throw new UnsupportedOperationException("The function is not supported yet: " + ctx.getText()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| /* | ||
| * Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| * All rights reserved. This program and the accompanying materials | ||
| * are made available under the terms of the Eclipse Public License v1.0 | ||
| * and Apache License v2.0 which accompanies this distribution. | ||
| * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | ||
| * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. | ||
| * You may elect to redistribute this code under either of these licenses. | ||
| * Contributors: | ||
| * Matheus Oliveira | ||
| */ | ||
| package org.eclipse.jnosql.communication.query.data; | ||
|
|
||
| import org.assertj.core.api.SoftAssertions; | ||
| import org.eclipse.jnosql.communication.Condition; | ||
| import org.eclipse.jnosql.communication.query.Function; | ||
| import org.eclipse.jnosql.communication.query.FunctionQueryValue; | ||
| import org.eclipse.jnosql.communication.query.ParamQueryValue; | ||
| import org.eclipse.jnosql.communication.query.QueryCondition; | ||
| import org.eclipse.jnosql.communication.query.SelectQuery; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
|
|
||
| import java.util.stream.Stream; | ||
|
|
||
| class SelectJakartaDataQueryFunctionTest { | ||
|
|
||
| private SelectParser selectParser; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| selectParser = new SelectParser(); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Should parse function expressions in WHERE clause") | ||
| @MethodSource("functionsProvider") | ||
| void shouldHandleFunctions(String query, String fieldName, String functionName) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good one, that is nice |
||
| SelectQuery selectQuery = selectParser.apply(query, "Customer"); | ||
|
|
||
| SoftAssertions.assertSoftly(soft -> { | ||
| soft.assertThat(selectQuery.where()).as("where clause is present").isPresent(); | ||
| QueryCondition condition = selectQuery.where().get().condition(); | ||
| soft.assertThat(condition.name()).as("condition field name").isEqualTo(fieldName); | ||
| soft.assertThat(condition.condition()).as("condition type is EQUALS").isEqualTo(Condition.EQUALS); | ||
| soft.assertThat(condition.value()).as("condition value is a FunctionQueryValue").isInstanceOf(FunctionQueryValue.class); | ||
| Function function = ((FunctionQueryValue) condition.value()).get(); | ||
| soft.assertThat(function.name()).as("function name").isEqualTo(functionName); | ||
| soft.assertThat(function.params()).as("function has parameters").isNotEmpty(); | ||
| }); | ||
| } | ||
|
|
||
| static Stream<Arguments> functionsProvider() { | ||
| return Stream.of( | ||
| Arguments.of("FROM Customer WHERE name = LOWER('JOHN')", "name", "LOWER"), | ||
| Arguments.of("FROM Customer WHERE name = lower('JOHN')", "name", "LOWER"), | ||
| Arguments.of("FROM Customer WHERE name = UPPER('john')", "name", "UPPER"), | ||
| Arguments.of("FROM Customer WHERE name = LENGTH('john')", "name", "LENGTH"), | ||
| Arguments.of("FROM Customer WHERE age = ABS(-10)", "age", "ABS"), | ||
| Arguments.of("FROM Customer WHERE name = LEFT('Jonathan', 2)", "name", "LEFT"), | ||
| Arguments.of("FROM Customer WHERE name = RIGHT('Jonathan', 1)", "name", "RIGHT") | ||
| ); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Should parse nested function expressions") | ||
| @MethodSource("nestedFunctionsProvider") | ||
| void shouldHandleNestedFunctions(String query, String outerFunc, String innerFunc) { | ||
| SelectQuery selectQuery = selectParser.apply(query, "Customer"); | ||
|
|
||
| SoftAssertions.assertSoftly(soft -> { | ||
| soft.assertThat(selectQuery.where()).as("where clause is present").isPresent(); | ||
| QueryCondition condition = selectQuery.where().get().condition(); | ||
| soft.assertThat(condition.value()).as("outer value is a FunctionQueryValue").isInstanceOf(FunctionQueryValue.class); | ||
| Function outer = ((FunctionQueryValue) condition.value()).get(); | ||
| soft.assertThat(outer.name()).as("outer function name").isEqualTo(outerFunc); | ||
| soft.assertThat(outer.params()[0]).as("inner param is a FunctionQueryValue").isInstanceOf(FunctionQueryValue.class); | ||
| Function inner = ((FunctionQueryValue) outer.params()[0]).get(); | ||
| soft.assertThat(inner.name()).as("inner function name").isEqualTo(innerFunc); | ||
| }); | ||
| } | ||
|
|
||
| static Stream<Arguments> nestedFunctionsProvider() { | ||
| return Stream.of( | ||
| Arguments.of("FROM Customer WHERE name = UPPER(LOWER(:name))", "UPPER", "LOWER"), | ||
| Arguments.of("FROM Customer WHERE name = LOWER(UPPER('john'))", "LOWER", "UPPER") | ||
| ); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Should parse function expressions with named parameters") | ||
| @MethodSource("parametersProvider") | ||
| void shouldHandleFunctionsWithParameters(String query, String functionName, String paramName) { | ||
| SelectQuery selectQuery = selectParser.apply(query, "Customer"); | ||
|
|
||
| SoftAssertions.assertSoftly(soft -> { | ||
| soft.assertThat(selectQuery.where()).as("where clause is present").isPresent(); | ||
| QueryCondition condition = selectQuery.where().get().condition(); | ||
| soft.assertThat(condition.condition()).as("condition type is EQUALS").isEqualTo(Condition.EQUALS); | ||
| soft.assertThat(condition.value()).as("condition value is a FunctionQueryValue").isInstanceOf(FunctionQueryValue.class); | ||
| Function function = ((FunctionQueryValue) condition.value()).get(); | ||
| soft.assertThat(function.name()).as("function name").isEqualTo(functionName); | ||
| soft.assertThat(function.params()[0]).as("first param is a ParamQueryValue").isInstanceOf(ParamQueryValue.class); | ||
| soft.assertThat(((ParamQueryValue) function.params()[0]).get()).as("param name").isEqualTo(paramName); | ||
| }); | ||
| } | ||
|
|
||
| static Stream<Arguments> parametersProvider() { | ||
| return Stream.of( | ||
| Arguments.of("FROM Customer WHERE name = LOWER(:name)", "LOWER", "name"), | ||
| Arguments.of("FROM Customer WHERE name = UPPER(?1)", "UPPER", "?1"), | ||
| Arguments.of("FROM Customer WHERE age = ABS((:age))", "ABS", "age") | ||
| ); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Should allow function keywords as field names") | ||
| @MethodSource("fieldCollisionProvider") | ||
| void shouldHandleFieldNamesSameAsFunctionNames(String query, String fieldName) { | ||
| SelectQuery selectQuery = selectParser.apply(query, "Box"); | ||
|
|
||
| SoftAssertions.assertSoftly(soft -> { | ||
| soft.assertThat(selectQuery.where()).as("where clause is present").isPresent(); | ||
| QueryCondition condition = selectQuery.where().get().condition(); | ||
| soft.assertThat(condition.name()).as("condition field name").isEqualTo(fieldName); | ||
| }); | ||
| } | ||
|
|
||
| static Stream<Arguments> fieldCollisionProvider() { | ||
| return Stream.of( | ||
| Arguments.of("FROM Box WHERE length = 10", "length"), | ||
| Arguments.of("FROM Box WHERE ABS(length) = 10", "ABS(length)"), | ||
| Arguments.of("FROM Box WHERE left = 'a'", "left") | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same question about visibility here.