Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version
- Include Is annotation support to the Repository
- Add support to Mapper on Key-value databases
- Include new engine to Jakarta Data Repositories
- Add support for scalar function expressions (ABS, LENGTH, LOWER, UPPER, LEFT, RIGHT) in JDQL string queries and in the fluent query API

=== Removed

Expand Down
26 changes: 26 additions & 0 deletions MAPPING.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,32 @@ DocumentTemplate template = // instance;
Stream<Person> result = template.query("select * from Person where age > 10");
----

===== Support for Functions in Text Queries

Eclipse JNoSQL supports several built-in functions in string-based queries (JDQL). These functions can be used in the `WHERE` clause to manipulate values or perform calculations during query execution.

Supported functions:

* *ABS(n)*: Returns the absolute value of a number.
* *LENGTH(s)*: Returns the number of characters in a string.
* *LOWER(s)*: Converts a string to lowercase.
* *UPPER(s)*: Converts a string to uppercase.
* *LEFT(s, n)*: Returns the first `n` characters of a string.
* *RIGHT(s, n)*: Returns the last `n` characters of a string.

[source,java]
----
Stream<Person> result = template.query("FROM Person WHERE LENGTH(name) > 5");
Stream<Person> electronics = template.query("FROM Product WHERE LOWER(category) = 'electronics'");

Stream<Person> joes = template.query("FROM Customer WHERE LEFT(name, 2) = 'Jo'");

PreparedStatement preparedStatement = template.prepare("FROM Customer WHERE LEFT(name, :len) = :prefix");
preparedStatement.bind("len", 2);
preparedStatement.bind("prefix", "Jo");
Stream<Customer> customers = preparedStatement.result();
----

===== Graph Database Types

If an application needs a recommendation engine or a full detail about the relationship between two entities in your system, it requires a Graph database type. A graph database contains a vertex and an edge. The edge is an object that holds the relationship information about the edges and has direction and properties that make it perfect for maps or human relationship. For the Graph API, Eclipse JNoSQL uses the Apache Tinkerpop. Likewise, the `GraphTemplate` is a wrapper to convert a Java entity to a `Vertex` in TinkerPop.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,7 @@ primary_expression
;

function_expression
: ('abs(' | 'ABS(') scalar_expression ')'
| ('length(' | 'LENGTH(') scalar_expression ')'
| ('lower(' | 'LOWER(') scalar_expression ')'
| ('upper(' | 'UPPER(') scalar_expression ')'
| ('left(' | 'LEFT(') scalar_expression ',' scalar_expression ')'
| ('right(' | 'RIGHT(') scalar_expression ',' scalar_expression ')'
: (ABS | LENGTH | LOWER | UPPER | LEFT | RIGHT) '(' scalar_expression (',' scalar_expression)* ')'
;

special_expression
Expand All @@ -84,9 +79,11 @@ special_expression
| FALSE
;

state_field_path_expression : IDENTIFIER (DOT IDENTIFIER)* | FULLY_QUALIFIED_IDENTIFIER | FUNCTION_ID;
state_field_path_expression : identifier (DOT identifier)* | FULLY_QUALIFIED_IDENTIFIER | FUNCTION_ID;

entity_name : IDENTIFIER; // no ambiguity
identifier : IDENTIFIER | ABS | LENGTH | LOWER | UPPER | LEFT | RIGHT;

entity_name : identifier; // no ambiguity

enum_literal : IDENTIFIER (DOT IDENTIFIER)* | FULLY_QUALIFIED_IDENTIFIER; // ambiguity with state_field_path_expression resolvable semantically

Expand Down Expand Up @@ -120,6 +117,12 @@ LOCAL_TIME : [lL][oO][cC][aA][lL] [tT][iI][mM][eE];
BETWEEN : [bB][eE][tT][wW][eE][eE][nN];
LIKE : [lL][iI][kK][eE];
THIS : [tT][hH][iI][sS];
ABS : [aA][bB][sS];
LENGTH : [lL][eE][nN][gG][tT][hH];
LOWER : [lL][oO][wW][eE][rR];
UPPER : [uU][pP][pP][eE][rR];
LEFT : [lL][eE][fF][tT];
RIGHT : [rR][iI][gG][hH][tT];
LOCAL : [lL][oO][cC][aA][lL];
DATE : [dD][aA][tT][eE];
DATETIME : [dD][aA][tT][eE][tT][iI][mM][eE];
Expand Down
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> {
Copy link
Copy Markdown
Contributor

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.


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
Expand Up @@ -8,6 +8,7 @@
* You may elect to redistribute this code under either of these licenses.
* Contributors:
* Otavio Santana
* Matheus Oliveira
*/
package org.eclipse.jnosql.communication.query.data;

Expand Down Expand Up @@ -185,11 +186,6 @@ public void exitIn_expression(JDQLParser.In_expressionContext ctx) {
and = andCondition;
}

@Override
public void exitFunction_expression(JDQLParser.Function_expressionContext ctx) {
throw new UnsupportedOperationException("The function is not supported in the query: " + ctx.getText());
}

private Condition getCondition(JDQLParser.Comparison_expressionContext ctx) {
var context = ctx.comparison_operator();
if (context.EQ() != null) {
Expand Down
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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
It is up to you.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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")
);
}
}
Loading