diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index af588bba6..c7359112c 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -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 diff --git a/MAPPING.adoc b/MAPPING.adoc index 10beb7d2c..b55575b34 100644 --- a/MAPPING.adoc +++ b/MAPPING.adoc @@ -562,6 +562,32 @@ DocumentTemplate template = // instance; Stream 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 result = template.query("FROM Person WHERE LENGTH(name) > 5"); +Stream electronics = template.query("FROM Product WHERE LOWER(category) = 'electronics'"); + +Stream 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 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. diff --git a/jnosql-communication/jnosql-communication-query/antlr4/org/eclipse/jnosql/query/grammar/data/JDQL.g4 b/jnosql-communication/jnosql-communication-query/antlr4/org/eclipse/jnosql/query/grammar/data/JDQL.g4 index 2ca307525..678750150 100644 --- a/jnosql-communication/jnosql-communication-query/antlr4/org/eclipse/jnosql/query/grammar/data/JDQL.g4 +++ b/jnosql-communication/jnosql-communication-query/antlr4/org/eclipse/jnosql/query/grammar/data/JDQL.g4 @@ -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 @@ -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 @@ -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]; diff --git a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/FunctionQueryValue.java b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/FunctionQueryValue.java new file mode 100644 index 000000000..6d46eb5b1 --- /dev/null +++ b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/FunctionQueryValue.java @@ -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 { + + 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(); + } +} diff --git a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/AbstractWhere.java b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/AbstractWhere.java index dd564b23e..55b932f70 100644 --- a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/AbstractWhere.java +++ b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/AbstractWhere.java @@ -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; @@ -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) { diff --git a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/DefaultFunction.java b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/DefaultFunction.java new file mode 100644 index 000000000..76ab2da07 --- /dev/null +++ b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/DefaultFunction.java @@ -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 { + + public DefaultFunction { + Objects.requireNonNull(name, "name is required"); + Objects.requireNonNull(params, "params is required"); + } + + @Override + public String toString() { + return name + "(" + Arrays.toString(params) + ")"; + } +} diff --git a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/PrimaryFunction.java b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/PrimaryFunction.java index c1e20b98e..8471307f7 100644 --- a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/PrimaryFunction.java +++ b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/PrimaryFunction.java @@ -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()); } -} \ No newline at end of file + + 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) { + 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()); + } +} diff --git a/jnosql-communication/jnosql-communication-query/src/test/java/org/eclipse/jnosql/communication/query/data/SelectJakartaDataQueryFunctionTest.java b/jnosql-communication/jnosql-communication-query/src/test/java/org/eclipse/jnosql/communication/query/data/SelectJakartaDataQueryFunctionTest.java new file mode 100644 index 000000000..6526f44cc --- /dev/null +++ b/jnosql-communication/jnosql-communication-query/src/test/java/org/eclipse/jnosql/communication/query/data/SelectJakartaDataQueryFunctionTest.java @@ -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) { + 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 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 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 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 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") + ); + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractMapperQuery.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractMapperQuery.java index 502f731ed..d25deebe4 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractMapperQuery.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractMapperQuery.java @@ -11,6 +11,7 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; @@ -40,6 +41,8 @@ abstract class AbstractMapperQuery { protected String name; + protected String nameForCondition; + protected final transient EntityMetadata mapping; protected final transient Converters converters; @@ -64,6 +67,28 @@ abstract class AbstractMapperQuery { }); } + protected String toFunctionExpression(Function function) { + String column = mapping.columnField(function.field()); + var sb = new StringBuilder(function.name()).append('(').append(column); + for (Object arg : function.arguments()) { + sb.append(", ").append(arg); + } + return sb.append(')').toString(); + } + + protected void setFunction(Function function) { + requireNonNull(function, "function is required"); + if (template instanceof AbstractSemiStructuredTemplate base) { + base.checkFunctionSupport(function); + } + this.name = function.field(); + this.nameForCondition = toFunctionExpression(function); + } + + protected String resolveColumnName() { + return nameForCondition != null ? nameForCondition : mapping.columnField(name); + } + protected void appendCondition(CriteriaCondition incomingCondition) { CriteriaCondition columnCondition = getCondition(incomingCondition); @@ -75,13 +100,14 @@ protected void appendCondition(CriteriaCondition incomingCondition) { this.negate = false; this.name = null; + this.nameForCondition = null; } protected void betweenImpl(T valueA, T valueB) { requireNonNull(valueA, "valueA is required"); requireNonNull(valueB, "valueB is required"); CriteriaCondition newCondition = CriteriaCondition - .between(Element.of(mapping.columnField(name), asList(getValue(valueA), getValue(valueB)))); + .between(Element.of(resolveColumnName(), asList(getValue(valueA), getValue(valueB)))); appendCondition(newCondition); } @@ -92,7 +118,7 @@ protected void inImpl(Iterable values) { List convertedValues = StreamSupport.stream(values.spliterator(), false) .map(this::getValue).collect(toList()); CriteriaCondition newCondition = CriteriaCondition - .in(Element.of(mapping.columnField(name), convertedValues)); + .in(Element.of(resolveColumnName(), convertedValues)); appendCondition(newCondition); } @@ -100,68 +126,72 @@ protected void eqImpl(T value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .eq(Element.of(mapping.columnField(name), getValue(value))); + .eq(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void likeImpl(String value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .like(Element.of(mapping.columnField(name), getValue(value))); + .like(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void gteImpl(T value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .gte(Element.of(mapping.columnField(name), getValue(value))); + .gte(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void gtImpl(T value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .gt(Element.of(mapping.columnField(name), getValue(value))); + .gt(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void ltImpl(T value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .lt(Element.of(mapping.columnField(name), getValue(value))); + .lt(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void lteImpl(T value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .lte(Element.of(mapping.columnField(name), getValue(value))); + .lte(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void containsImpl(String value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .contains(Element.of(mapping.columnField(name), getValue(value))); + .contains(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void startWithImpl(String value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .startsWith(Element.of(mapping.columnField(name), getValue(value))); + .startsWith(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected void endsWithImpl(String value) { requireNonNull(value, "value is required"); CriteriaCondition newCondition = CriteriaCondition - .endsWith(Element.of(mapping.columnField(name), getValue(value))); + .endsWith(Element.of(resolveColumnName(), getValue(value))); appendCondition(newCondition); } protected Object getValue(Object value) { + // skip type conversion when a function is active; the value targets the function result type + if (nameForCondition != null) { + return value; + } return ConverterUtil.getValue(value, mapping, name, converters); } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractSemiStructuredTemplate.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractSemiStructuredTemplate.java index 0863f2f30..650844f77 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractSemiStructuredTemplate.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/AbstractSemiStructuredTemplate.java @@ -11,6 +11,7 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira * Maximillian Arruda */ package org.eclipse.jnosql.mapping.semistructured; @@ -22,7 +23,6 @@ import jakarta.data.page.PageRequest; import jakarta.data.page.impl.CursoredPageRecord; import jakarta.nosql.Query; -import jakarta.nosql.QueryMapper; import jakarta.nosql.TypedQuery; import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; import org.eclipse.jnosql.communication.semistructured.DatabaseManager; @@ -282,21 +282,21 @@ private Stream executeQuery(SelectQuery query) { } @Override - public QueryMapper.MapperFrom select(Class type) { + public SemiStructuredMapperSelect select(Class type) { requireNonNull(type, "type is required"); EntityMetadata metadata = entities().get(type); return new MapperSelect(metadata, converters(), this); } @Override - public QueryMapper.MapperDeleteFrom delete(Class type) { + public SemiStructuredMapperDelete delete(Class type) { requireNonNull(type, "type is required"); EntityMetadata metadata = entities().get(type); return new MapperDelete(metadata, converters(), this); } @Override - public QueryMapper.MapperUpdateFrom update(Class type) { + public SemiStructuredMapperUpdate update(Class type) { requireNonNull(type, "type is required"); EntityMetadata metadata = entities().get(type); return new MapperUpdate(metadata, converters(), this); @@ -397,6 +397,19 @@ protected T persist(T entity, UnaryOperator persistActi .orElseThrow(); } + /** + * Checks whether this template's database supports the given function expression. + * Database drivers that support scalar functions should override this method and return normally. + * The default implementation always throws {@link UnsupportedFunctionException}. + * + * @param function the function expression to validate + * @throws UnsupportedFunctionException if the underlying database does not support the function + * @since 1.1.0 + */ + protected void checkFunctionSupport(org.eclipse.jnosql.mapping.semistructured.Function function) { + throw new UnsupportedFunctionException(function.name(), manager().getClass().getSimpleName()); + } + private UnaryOperator toUnary(Consumer consumer) { return t -> { consumer.accept(t); diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/Function.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/Function.java new file mode 100644 index 000000000..d723f35b1 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/Function.java @@ -0,0 +1,179 @@ +/* + * 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.mapping.semistructured; + +import java.util.Objects; + +/** + * Represents a scalar function expression that can be applied to entity fields in queries. + * Function expressions allow operations such as UPPER, LOWER, LEFT, RIGHT, LENGTH, and ABS + * to be used in the fluent query API. + * + *

Example usage:

+ *
{@code
+ * List words = template.select(Word.class)
+ *         .where(Function.upper("term"))
+ *         .eq("JAVA")
+ *         .result();
+ *
+ * List result = template.select(Word.class)
+ *         .where(Function.left("term", 2)).eq("Ja")
+ *         .and(Function.length("term")).gt(5)
+ *         .result();
+ * }
+ * + *

When a function is not supported by the underlying database, an + * {@link UnsupportedFunctionException} may be thrown by the database driver. + * Drivers that support scalar functions should override + * {@link AbstractSemiStructuredTemplate#checkFunctionSupport(org.eclipse.jnosql.mapping.semistructured.Function)}.

+ * + * @since 1.1.0 + */ +public interface Function { + + /** + * Returns the name of the function (e.g., {@code "UPPER"}, {@code "LEFT"}, {@code "ABS"}). + * + * @return the function name, never {@code null} + */ + String name(); + + /** + * Returns the entity field name this function operates on. + * + * @return the field name, never {@code null} + */ + String field(); + + /** + * Returns additional arguments passed to this function. + * For single-argument functions (e.g., UPPER, LOWER), this returns an empty array. + * + * @return an array of function arguments, never {@code null} + */ + Object[] arguments(); + + /** + * Creates a {@code LEFT(field, length)} function expression. + * + * @param field the entity field name + * @param length the number of characters to extract from the left + * @return a LEFT function expression + * @throws NullPointerException if {@code field} is {@code null} + * @throws IllegalArgumentException if {@code length} is negative + */ + static Function left(String field, int length) { + Objects.requireNonNull(field, "field is required"); + if (length < 0) { + throw new IllegalArgumentException("length must be non-negative"); + } + return new DefaultFunction("LEFT", field, length); + } + + /** + * Creates a {@code RIGHT(field, length)} function expression. + * + * @param field the entity field name + * @param length the number of characters to extract from the right + * @return a RIGHT function expression + * @throws NullPointerException if {@code field} is {@code null} + * @throws IllegalArgumentException if {@code length} is negative + */ + static Function right(String field, int length) { + Objects.requireNonNull(field, "field is required"); + if (length < 0) { + throw new IllegalArgumentException("length must be non-negative"); + } + return new DefaultFunction("RIGHT", field, length); + } + + /** + * Creates an {@code UPPER(field)} function expression. + * + * @param field the entity field name + * @return an UPPER function expression + * @throws NullPointerException if {@code field} is {@code null} + */ + static Function upper(String field) { + Objects.requireNonNull(field, "field is required"); + return new DefaultFunction("UPPER", field); + } + + /** + * Creates a {@code LOWER(field)} function expression. + * + * @param field the entity field name + * @return a LOWER function expression + * @throws NullPointerException if {@code field} is {@code null} + */ + static Function lower(String field) { + Objects.requireNonNull(field, "field is required"); + return new DefaultFunction("LOWER", field); + } + + /** + * Creates a {@code LENGTH(field)} function expression. + * + * @param field the entity field name + * @return a LENGTH function expression + * @throws NullPointerException if {@code field} is {@code null} + */ + static Function length(String field) { + Objects.requireNonNull(field, "field is required"); + return new DefaultFunction("LENGTH", field); + } + + /** + * Creates an {@code ABS(field)} function expression. + * + * @param field the entity field name + * @return an ABS function expression + * @throws NullPointerException if {@code field} is {@code null} + */ + static Function abs(String field) { + Objects.requireNonNull(field, "field is required"); + return new DefaultFunction("ABS", field); + } + + /** + * Default immutable implementation of {@link Function} using a Java record. + * + * @param name the function name + * @param field the entity field name + * @param arguments optional additional arguments + */ + record DefaultFunction(String name, String field, Object... arguments) implements Function { + + public DefaultFunction { + Objects.requireNonNull(name, "name is required"); + Objects.requireNonNull(field, "field is required"); + arguments = arguments == null ? new Object[0] : arguments.clone(); + } + + @Override + public Object[] arguments() { + return arguments.clone(); + } + + @Override + public String toString() { + var sb = new StringBuilder(name).append('(').append(field); + for (var arg : arguments) { + sb.append(", ").append(arg); + } + return sb.append(')').toString(); + } + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperDelete.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperDelete.java index 5b759de77..8e84cb077 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperDelete.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperDelete.java @@ -11,124 +11,142 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; -import jakarta.nosql.QueryMapper.MapperDeleteFrom; -import jakarta.nosql.QueryMapper.MapperDeleteNameCondition; -import jakarta.nosql.QueryMapper.MapperDeleteNotCondition; -import jakarta.nosql.QueryMapper.MapperDeleteWhere; import org.eclipse.jnosql.communication.semistructured.DeleteQuery; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; import static java.util.Objects.requireNonNull; -final class MapperDelete extends AbstractMapperQuery implements MapperDeleteFrom, - MapperDeleteWhere, MapperDeleteNameCondition, MapperDeleteNotCondition { - +final class MapperDelete extends AbstractMapperQuery implements SemiStructuredMapperDelete { MapperDelete(EntityMetadata mapping, Converters converters, SemiStructuredTemplate template) { super(mapping, converters, template); } @Override - public MapperDeleteNameCondition where(String name) { + public SemiStructuredMapperDelete where(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; return this; } - @Override - public MapperDeleteNameCondition and(String name) { + public SemiStructuredMapperDelete and(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; this.and = true; return this; } @Override - public MapperDeleteNameCondition or(String name) { + public SemiStructuredMapperDelete or(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; this.and = false; return this; } - @Override - public MapperDeleteNotCondition not() { + public SemiStructuredMapperDelete not() { this.negate = true; return this; } @Override - public MapperDeleteWhere eq(T value) { + public SemiStructuredMapperDelete eq(T value) { eqImpl(value); return this; } @Override - public MapperDeleteWhere like(String value) { + public SemiStructuredMapperDelete like(String value) { likeImpl(value); return this; } @Override - public MapperDeleteWhere contains(String value) { + public SemiStructuredMapperDelete contains(String value) { containsImpl(value); return this; } @Override - public MapperDeleteWhere startsWith(String value) { + public SemiStructuredMapperDelete startsWith(String value) { startWithImpl(value); return this; } @Override - public MapperDeleteWhere endsWith(String value) { + public SemiStructuredMapperDelete endsWith(String value) { endsWithImpl(value); return this; } @Override - public MapperDeleteWhere gt(T value) { + public SemiStructuredMapperDelete gt(T value) { gtImpl(value); return this; } @Override - public MapperDeleteWhere gte(T value) { + public SemiStructuredMapperDelete gte(T value) { gteImpl(value); return this; } @Override - public MapperDeleteWhere lt(T value) { + public SemiStructuredMapperDelete lt(T value) { ltImpl(value); return this; } @Override - public MapperDeleteWhere lte(T value) { + public SemiStructuredMapperDelete lte(T value) { lteImpl(value); return this; } @Override - public MapperDeleteWhere between(T valueA, T valueB) { + public SemiStructuredMapperDelete between(T valueA, T valueB) { betweenImpl(valueA, valueB); return this; } @Override - public MapperDeleteWhere in(Iterable values) { + public SemiStructuredMapperDelete in(Iterable values) { inImpl(values); return this; } + @Override + public SemiStructuredMapperDelete where(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + return this; + } + + @Override + public SemiStructuredMapperDelete and(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + this.and = true; + return this; + } + + @Override + public SemiStructuredMapperDelete or(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + this.and = false; + return this; + } private DeleteQuery build() { return new MappingDeleteQuery(entity, condition); diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperSelect.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperSelect.java index 276275068..fd65ac37e 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperSelect.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperSelect.java @@ -11,19 +11,12 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; import jakarta.data.Direction; import jakarta.data.Sort; -import jakarta.nosql.QueryMapper.MapperFrom; -import jakarta.nosql.QueryMapper.MapperLimit; -import jakarta.nosql.QueryMapper.MapperNameCondition; -import jakarta.nosql.QueryMapper.MapperNameOrder; -import jakarta.nosql.QueryMapper.MapperNotCondition; -import jakarta.nosql.QueryMapper.MapperOrder; -import jakarta.nosql.QueryMapper.MapperSkip; -import jakarta.nosql.QueryMapper.MapperWhere; import org.eclipse.jnosql.communication.semistructured.SelectQuery; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; @@ -35,9 +28,7 @@ import static java.util.Objects.requireNonNull; -final class MapperSelect extends AbstractMapperQuery implements MapperFrom, MapperLimit, - MapperSkip, MapperOrder, MapperNameCondition, - MapperNotCondition, MapperNameOrder, MapperWhere { +final class MapperSelect extends AbstractMapperQuery implements SemiStructuredMapperSelect { private final List> sorts = new ArrayList<>(); @@ -46,132 +37,167 @@ final class MapperSelect extends AbstractMapperQuery implements MapperFrom, Mapp } @Override - public MapperNameCondition and(String name) { + public SemiStructuredMapperSelect and(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; this.and = true; return this; } @Override - public MapperNameCondition or(String name) { + public SemiStructuredMapperSelect or(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; this.and = false; return this; } @Override - public MapperNameCondition where(String name) { + public SemiStructuredMapperSelect where(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; return this; } @Override - public MapperSkip skip(long start) { + public SemiStructuredMapperSelect skip(long start) { this.start = start; return this; } @Override - public MapperLimit limit(long limit) { + public SemiStructuredMapperSelect limit(long limit) { this.limit = limit; return this; } @Override - public MapperOrder orderBy(String name) { + public SemiStructuredMapperSelect orderBy(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; return this; } @Override - public MapperNotCondition not() { + public SemiStructuredMapperSelect not() { this.negate = true; return this; } @Override - public MapperWhere eq(T value) { + public SemiStructuredMapperSelect eq(T value) { eqImpl(value); return this; } - @Override - public MapperWhere like(String value) { + public SemiStructuredMapperSelect like(String value) { likeImpl(value); return this; } @Override - public MapperWhere contains(String value) { + public SemiStructuredMapperSelect contains(String value) { containsImpl(value); return this; } @Override - public MapperWhere startsWith(String value) { + public SemiStructuredMapperSelect startsWith(String value) { startWithImpl(value); return this; } @Override - public MapperWhere endsWith(String value) { + public SemiStructuredMapperSelect endsWith(String value) { endsWithImpl(value); return this; } @Override - public MapperWhere gt(T value) { + public SemiStructuredMapperSelect gt(T value) { gtImpl(value); return this; } @Override - public MapperWhere gte(T value) { + public SemiStructuredMapperSelect gte(T value) { gteImpl(value); return this; } @Override - public MapperWhere lt(T value) { + public SemiStructuredMapperSelect lt(T value) { ltImpl(value); return this; } - @Override - public MapperWhere lte(T value) { + public SemiStructuredMapperSelect lte(T value) { lteImpl(value); return this; } @Override - public MapperWhere between(T valueA, T valueB) { + public SemiStructuredMapperSelect between(T valueA, T valueB) { betweenImpl(valueA, valueB); return this; } @Override - public MapperWhere in(Iterable values) { + public SemiStructuredMapperSelect in(Iterable values) { inImpl(values); return this; } @Override - public MapperNameOrder asc() { - this.sorts.add(Sort.of(mapping.columnField(name), Direction.ASC, false)); + public SemiStructuredMapperSelect asc() { + this.sorts.add(Sort.of(resolveColumnName(), Direction.ASC, false)); + this.nameForCondition = null; + return this; + } + + @Override + public SemiStructuredMapperSelect desc() { + this.sorts.add(Sort.of(resolveColumnName(), Direction.DESC, false)); + this.nameForCondition = null; return this; } @Override - public MapperNameOrder desc() { - this.sorts.add(Sort.of(mapping.columnField(name), Direction.DESC, false)); + public SemiStructuredMapperSelect where(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + return this; + } + + @Override + public SemiStructuredMapperSelect and(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + this.and = true; return this; } + + @Override + public SemiStructuredMapperSelect or(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + this.and = false; + return this; + } + + @Override + public SemiStructuredMapperSelect orderBy(Function function) { + requireNonNull(function, "function is required"); + this.nameForCondition = toFunctionExpression(function); + return this; + } + private SelectQuery build() { return new MappingQuery(sorts, limit, start, condition, entity, List.of()); } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdate.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdate.java index c4665c09a..03cebef56 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdate.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdate.java @@ -11,11 +11,11 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira * Maximillian Arruda */ package org.eclipse.jnosql.mapping.semistructured; -import jakarta.nosql.QueryMapper; import org.eclipse.jnosql.communication.semistructured.Element; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; @@ -26,14 +26,7 @@ import static java.util.Objects.requireNonNull; -final class MapperUpdate extends AbstractMapperQuery implements - QueryMapper.MapperUpdateFrom, - QueryMapper.MapperUpdateSetTo, - QueryMapper.MapperUpdateSetStep, - QueryMapper.MapperUpdateNameCondition, - QueryMapper.MapperUpdateWhere, - QueryMapper.MapperUpdateNotCondition, - QueryMapper.MapperUpdateQueryBuild { +final class MapperUpdate extends AbstractMapperQuery implements SemiStructuredMapperUpdate { private final List elements = new LinkedList<>(); @@ -42,110 +35,136 @@ final class MapperUpdate extends AbstractMapperQuery implements } @Override - public QueryMapper.MapperUpdateSetTo set(String name) { + public SemiStructuredMapperUpdate set(String name) { requireNonNull(name, "name is required"); this.name = name; return this; } @Override - public QueryMapper.MapperUpdateSetStep to(T value) { + public SemiStructuredMapperUpdate to(T value) { requireNonNull(this.name, "name is required"); this.elements.add(Element.of(mapping.columnField(name), getValue(value))); return this; } @Override - public QueryMapper.MapperUpdateNameCondition where(String name) { + public SemiStructuredMapperUpdate where(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; return this; } @Override - public QueryMapper.MapperUpdateWhere eq(T value) { + public SemiStructuredMapperUpdate eq(T value) { eqImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere like(String value) { + public SemiStructuredMapperUpdate like(String value) { likeImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere contains(String value) { + public SemiStructuredMapperUpdate contains(String value) { containsImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere startsWith(String value) { + public SemiStructuredMapperUpdate startsWith(String value) { startWithImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere endsWith(String value) { + public SemiStructuredMapperUpdate endsWith(String value) { endsWithImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere gt(T value) { + public SemiStructuredMapperUpdate gt(T value) { gtImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere gte(T value) { + public SemiStructuredMapperUpdate gte(T value) { gteImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere lt(T value) { + public SemiStructuredMapperUpdate lt(T value) { ltImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere lte(T value) { + public SemiStructuredMapperUpdate lte(T value) { lteImpl(value); return this; } @Override - public QueryMapper.MapperUpdateWhere between(T valueA, T valueB) { + public SemiStructuredMapperUpdate between(T valueA, T valueB) { betweenImpl(valueA, valueB); return this; } @Override - public QueryMapper.MapperUpdateWhere in(Iterable values) { + public SemiStructuredMapperUpdate in(Iterable values) { inImpl(values); return this; } @Override - public QueryMapper.MapperUpdateNotCondition not() { + public SemiStructuredMapperUpdate not() { this.negate = true; return this; } @Override - public QueryMapper.MapperUpdateNameCondition and(String name) { + public SemiStructuredMapperUpdate and(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; this.and = true; return this; } @Override - public QueryMapper.MapperUpdateNameCondition or(String name) { + public SemiStructuredMapperUpdate or(String name) { requireNonNull(name, "name is required"); this.name = name; + this.nameForCondition = null; + this.and = false; + return this; + } + + @Override + public SemiStructuredMapperUpdate where(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + return this; + } + + @Override + public SemiStructuredMapperUpdate and(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); + this.and = true; + return this; + } + + @Override + public SemiStructuredMapperUpdate or(Function function) { + requireNonNull(function, "function is required"); + setFunction(function); this.and = false; return this; } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperDelete.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperDelete.java new file mode 100644 index 000000000..9fa0ebf77 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperDelete.java @@ -0,0 +1,111 @@ +/* + * 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.mapping.semistructured; + +import jakarta.nosql.QueryMapper; + +/** + * A fluent delete query builder for semi-structured databases that extends the standard + * {@link QueryMapper} fluent API with support for {@link Function} expressions. + * + *

Returned by {@link AbstractSemiStructuredTemplate#delete(Class)}, this interface + * combines delete query-building concerns with function expression support.

+ * + *

Example usage:

+ *
{@code
+ * template.delete(Word.class)
+ *         .where(Function.upper("term")).eq("JAVA")
+ *         .execute();
+ * }
+ * + * @since 1.1.0 + * @see Function + */ +public interface SemiStructuredMapperDelete extends + QueryMapper.MapperDeleteFrom, + QueryMapper.MapperDeleteWhere, + QueryMapper.MapperDeleteNameCondition, + QueryMapper.MapperDeleteNotCondition, + QueryMapper.MapperDeleteQueryBuild { + + @Override + SemiStructuredMapperDelete where(String name); + + @Override + SemiStructuredMapperDelete eq(T value); + + @Override + SemiStructuredMapperDelete gt(T value); + + @Override + SemiStructuredMapperDelete gte(T value); + + @Override + SemiStructuredMapperDelete lt(T value); + + @Override + SemiStructuredMapperDelete lte(T value); + + @Override + SemiStructuredMapperDelete like(String value); + + @Override + SemiStructuredMapperDelete contains(String value); + + @Override + SemiStructuredMapperDelete startsWith(String value); + + @Override + SemiStructuredMapperDelete endsWith(String value); + + @Override + SemiStructuredMapperDelete between(T valueA, T valueB); + + @Override + SemiStructuredMapperDelete not(); + + @Override + SemiStructuredMapperDelete and(String name); + + @Override + SemiStructuredMapperDelete or(String name); + + /** + * Starts a WHERE clause using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperDelete where(Function function); + + /** + * Adds an AND condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperDelete and(Function function); + + /** + * Adds an OR condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperDelete or(Function function); +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperSelect.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperSelect.java new file mode 100644 index 000000000..3676a2b20 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperSelect.java @@ -0,0 +1,145 @@ +/* + * 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.mapping.semistructured; + +import jakarta.nosql.QueryMapper; + +/** + * A fluent select query builder for semi-structured databases that extends the standard + * {@link QueryMapper} fluent API with support for {@link Function} expressions. + * + *

Returned by {@link SemiStructuredTemplate#select(Class)} (and + * {@link AbstractSemiStructuredTemplate}), this interface combines all query-building + * concerns — field conditions, logical operators, ordering, pagination, and + * function expressions — into a single fluent type.

+ * + *

Example usage:

+ *
{@code
+ * List result = template.select(Word.class)
+ *         .where(Function.upper("term")).eq("JAVA")
+ *         .and(Function.length("term")).gt(5)
+ *         .result();
+ * }
+ * + * @since 1.1.0 + * @see Function + */ +public interface SemiStructuredMapperSelect extends + QueryMapper.MapperFrom, + QueryMapper.MapperWhere, + QueryMapper.MapperNameCondition, + QueryMapper.MapperNotCondition, + QueryMapper.MapperNameOrder, + QueryMapper.MapperOrder, + QueryMapper.MapperSkip, + QueryMapper.MapperLimit, + QueryMapper.MapperQueryBuild { + + @Override + SemiStructuredMapperSelect where(String name); + + @Override + SemiStructuredMapperSelect eq(T value); + + @Override + SemiStructuredMapperSelect gt(T value); + + @Override + SemiStructuredMapperSelect gte(T value); + + @Override + SemiStructuredMapperSelect lt(T value); + + @Override + SemiStructuredMapperSelect lte(T value); + + @Override + SemiStructuredMapperSelect like(String value); + + @Override + SemiStructuredMapperSelect contains(String value); + + @Override + SemiStructuredMapperSelect startsWith(String value); + + @Override + SemiStructuredMapperSelect endsWith(String value); + + @Override + SemiStructuredMapperSelect between(T valueA, T valueB); + + @Override + SemiStructuredMapperSelect in(Iterable values); + + @Override + SemiStructuredMapperSelect not(); + + @Override + SemiStructuredMapperSelect and(String name); + + @Override + SemiStructuredMapperSelect or(String name); + + @Override + SemiStructuredMapperSelect skip(long skip); + + @Override + SemiStructuredMapperSelect limit(long limit); + + @Override + SemiStructuredMapperSelect orderBy(String name); + + @Override + SemiStructuredMapperSelect asc(); + + @Override + SemiStructuredMapperSelect desc(); + + /** + * Starts a WHERE clause using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperSelect where(Function function); + + /** + * Adds an AND condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperSelect and(Function function); + + /** + * Adds an OR condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperSelect or(Function function); + + /** + * Adds an ORDER BY clause using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperSelect orderBy(Function function); +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperUpdate.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperUpdate.java new file mode 100644 index 000000000..d3a8620ff --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredMapperUpdate.java @@ -0,0 +1,120 @@ +/* + * 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.mapping.semistructured; + +import jakarta.nosql.QueryMapper; + +/** + * A fluent update query builder for semi-structured databases that extends the standard + * {@link QueryMapper} fluent API with support for {@link Function} expressions. + * + *

Returned by {@link AbstractSemiStructuredTemplate#update(Class)}, this interface + * combines update query-building concerns with function expression support.

+ * + *

Example usage:

+ *
{@code
+ * template.update(Word.class)
+ *         .set("meaning").to("new meaning")
+ *         .where(Function.upper("term")).eq("JAVA")
+ *         .execute();
+ * }
+ * + * @since 1.1.0 + * @see Function + */ +public interface SemiStructuredMapperUpdate extends + QueryMapper.MapperUpdateFrom, + QueryMapper.MapperUpdateSetStep, + QueryMapper.MapperUpdateSetTo, + QueryMapper.MapperUpdateNameCondition, + QueryMapper.MapperUpdateWhere, + QueryMapper.MapperUpdateNotCondition, + QueryMapper.MapperUpdateQueryBuild { + + @Override + SemiStructuredMapperUpdate set(String name); + + @Override + SemiStructuredMapperUpdate to(T value); + + @Override + SemiStructuredMapperUpdate where(String name); + + @Override + SemiStructuredMapperUpdate eq(T value); + + @Override + SemiStructuredMapperUpdate gt(T value); + + @Override + SemiStructuredMapperUpdate gte(T value); + + @Override + SemiStructuredMapperUpdate lt(T value); + + @Override + SemiStructuredMapperUpdate lte(T value); + + @Override + SemiStructuredMapperUpdate like(String value); + + @Override + SemiStructuredMapperUpdate contains(String value); + + @Override + SemiStructuredMapperUpdate startsWith(String value); + + @Override + SemiStructuredMapperUpdate endsWith(String value); + + @Override + SemiStructuredMapperUpdate between(T valueA, T valueB); + + @Override + SemiStructuredMapperUpdate not(); + + @Override + SemiStructuredMapperUpdate and(String name); + + @Override + SemiStructuredMapperUpdate or(String name); + + /** + * Starts a WHERE clause using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperUpdate where(Function function); + + /** + * Adds an AND condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperUpdate and(Function function); + + /** + * Adds an OR condition using a {@link Function} expression. + * + * @param function the function expression; must not be {@code null} + * @return this builder + * @throws NullPointerException if {@code function} is {@code null} + */ + SemiStructuredMapperUpdate or(Function function); +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/UnsupportedFunctionException.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/UnsupportedFunctionException.java new file mode 100644 index 000000000..dd8fd4975 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/UnsupportedFunctionException.java @@ -0,0 +1,63 @@ +/* + * 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.mapping.semistructured; + +import jakarta.nosql.MappingException; + +/** + * Exception thrown when a query {@link Function} is not supported by the underlying NoSQL database. + * + *

Most NoSQL databases do not natively support scalar functions. This exception signals + * that the database provider cannot execute the requested function expression.

+ * + * @since 1.1.0 + * @see Function + */ +public class UnsupportedFunctionException extends MappingException { + + /** + * Constructs a new exception for an unsupported function on a specific database. + * + * @param functionName the name of the unsupported function + * @param databaseName the name of the database + */ + public UnsupportedFunctionException(String functionName, String databaseName) { + super(String.format( + "Function '%s' is not supported by %s. " + + "Consider using a database with SQL-compatible query support " + + "(e.g., Couchbase, Neo4j, Oracle NoSQL) " + + "or implement filtering at the application level.", + functionName, databaseName)); + } + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message + */ + public UnsupportedFunctionException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public UnsupportedFunctionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplate.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplate.java index cdd933d38..0e348c63b 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplate.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplate.java @@ -21,7 +21,6 @@ import org.eclipse.jnosql.communication.semistructured.DatabaseManager; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; - @Default @ApplicationScoped class DefaultSemiStructuredTemplate extends AbstractSemiStructuredTemplate { @@ -74,4 +73,9 @@ protected EntitiesMetadata entities() { protected Converters converters() { return converters; } + + @Override + protected void checkFunctionSupport(Function function) { + // no-op: test template accepts all operations; real database behavior is mocked + } } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperDeleteTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperDeleteTest.java index a2c355bc9..0a4512bf6 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperDeleteTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperDeleteTest.java @@ -11,11 +11,13 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.semistructured.Function; import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; import org.eclipse.jnosql.communication.semistructured.DatabaseManager; import org.eclipse.jnosql.communication.semistructured.DeleteQuery; @@ -32,6 +34,7 @@ import org.jboss.weld.junit5.auto.AddPackages; import org.jboss.weld.junit5.auto.EnableAutoWeld; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -41,6 +44,7 @@ import static org.eclipse.jnosql.communication.semistructured.DeleteQuery.delete; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.Mockito.when; @EnableAutoWeld @@ -281,4 +285,53 @@ void shouldQueryBySubEntity() { assertEquals(queryExpected, query); } + + @Test + @DisplayName("Should delete with UPPER function in where clause") + void shouldDeleteWhereFunctionUpper() { + template.delete(Person.class).where(Function.upper("name")).eq("ADA").execute(); + Mockito.verify(managerMock).delete(captor.capture()); + var query = captor.getValue(); + var queryExpected = delete().from("Person").where("UPPER(name)").eq("ADA").build(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should delete with LEFT function in where clause") + void shouldDeleteWhereFunctionLeft() { + template.delete(Person.class).where(Function.left("name", 2)).eq("Ad").execute(); + Mockito.verify(managerMock).delete(captor.capture()); + var query = captor.getValue(); + var queryExpected = delete().from("Person").where("LEFT(name, 2)").eq("Ad").build(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should delete with LOWER function in AND condition") + void shouldDeleteAndFunctionLower() { + template.delete(Person.class).where("age").gt(10).and(Function.lower("name")).eq("ada").execute(); + Mockito.verify(managerMock).delete(captor.capture()); + var query = captor.getValue(); + var queryExpected = delete().from("Person").where("age").gt(10) + .and("LOWER(name)").eq("ada").build(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should delete with UPPER function in OR condition") + void shouldDeleteOrFunctionUpper() { + template.delete(Person.class).where("age").gt(10).or(Function.upper("name")).eq("ADA").execute(); + Mockito.verify(managerMock).delete(captor.capture()); + var query = captor.getValue(); + var queryExpected = delete().from("Person").where("age").gt(10) + .or("UPPER(name)").eq("ADA").build(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should throw NullPointerException when function is null in where clause") + void shouldReturnErrorWhereFunctionIsNull() { + assertThatNullPointerException().isThrownBy( + () -> template.delete(Person.class).where((Function) null)); + } } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperSelectTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperSelectTest.java index b5d6fe8f6..4bd6b7e95 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperSelectTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperSelectTest.java @@ -11,11 +11,13 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.semistructured.Function; import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; import org.eclipse.jnosql.communication.semistructured.DatabaseManager; import org.eclipse.jnosql.communication.semistructured.Element; @@ -33,6 +35,7 @@ import org.jboss.weld.junit5.auto.EnableAutoWeld; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -43,6 +46,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.contains; import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.endsWith; import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.startsWith; @@ -385,7 +389,101 @@ void shouldSCount() { @Test void shouldReturnErrorSelectWhenOrderIsNull() { - Assertions.assertThrows(NullPointerException.class, () -> template.select(Worker.class).orderBy(null)); + Assertions.assertThrows(NullPointerException.class, () -> template.select(Worker.class).orderBy((String) null)); + } + + @Test + @DisplayName("Should select with UPPER function in where clause") + void shouldSelectWhereFunctionUpper() { + template.select(Person.class).where(Function.upper("name")).eq("ADA").result(); + SelectQuery queryExpected = select().from("Person").where("UPPER(name)") + .eq("ADA").build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with LOWER function in where clause") + void shouldSelectWhereFunctionLower() { + template.select(Person.class).where(Function.lower("name")).eq("ada").result(); + SelectQuery queryExpected = select().from("Person").where("LOWER(name)") + .eq("ada").build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with LEFT function in where clause") + void shouldSelectWhereFunctionLeft() { + template.select(Person.class).where(Function.left("name", 2)).eq("Ad").result(); + SelectQuery queryExpected = select().from("Person").where("LEFT(name, 2)") + .eq("Ad").build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with LENGTH function in where clause") + void shouldSelectWhereFunctionLength() { + template.select(Person.class).where(Function.length("name")).gt(3).result(); + SelectQuery queryExpected = select().from("Person").where("LENGTH(name)") + .gt(3).build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with ABS function in where clause") + void shouldSelectWhereFunctionAbs() { + template.select(Person.class).where(Function.abs("age")).gt(10).result(); + SelectQuery queryExpected = select().from("Person").where("ABS(age)") + .gt(10).build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with UPPER function in AND condition") + void shouldSelectAndFunctionUpper() { + template.select(Person.class).where("age").gt(10).and(Function.upper("name")).eq("ADA").result(); + SelectQuery queryExpected = select().from("Person").where("age").gt(10) + .and("UPPER(name)").eq("ADA").build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with LOWER function in OR condition") + void shouldSelectOrFunctionLower() { + template.select(Person.class).where("age").gt(10).or(Function.lower("name")).eq("ada").result(); + SelectQuery queryExpected = select().from("Person").where("age").gt(10) + .or("LOWER(name)").eq("ada").build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should select with UPPER function in orderBy clause") + void shouldSelectOrderByFunctionUpper() { + template.select(Person.class).orderBy(Function.upper("name")).asc().result(); + SelectQuery queryExpected = select().from("Person").orderBy("UPPER(name)").asc().build(); + Mockito.verify(managerMock).select(captor.capture()); + SelectQuery query = captor.getValue(); + assertEquals(queryExpected, query); + } + + @Test + @DisplayName("Should throw NullPointerException when function is null in where clause") + void shouldReturnErrorWhereFunctionIsNull() { + assertThatNullPointerException().isThrownBy( + () -> template.select(Person.class).where((Function) null)); } } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdateTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdateTest.java index 193476f5f..9a4e292e9 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdateTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/MapperUpdateTest.java @@ -11,6 +11,7 @@ * Contributors: * * Otavio Santana + * Matheus Oliveira */ package org.eclipse.jnosql.mapping.semistructured; @@ -18,6 +19,7 @@ import jakarta.inject.Inject; import org.assertj.core.api.SoftAssertions; import org.eclipse.jnosql.communication.Condition; +import org.eclipse.jnosql.mapping.semistructured.Function; import org.eclipse.jnosql.communication.TypeReference; import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; import org.eclipse.jnosql.communication.semistructured.DatabaseManager; @@ -373,4 +375,57 @@ void shouldUpdateWithEndsWithCondition() { }); } + @Test + @DisplayName("Should update with UPPER function in where clause") + void shouldUpdateWhereFunctionUpper() { + template.update(Person.class) + .set("name").to("Ada") + .where(Function.upper("name")).eq("ADA") + .execute(); + + Mockito.verify(managerMock).update(captor.capture()); + var update = captor.getValue(); + + SoftAssertions.assertSoftly(soft -> { + var condition = update.where().orElseThrow(); + soft.assertThat(condition).as("condition matches UPPER(name) = ADA") + .isEqualTo(CriteriaCondition.eq(Element.of("UPPER(name)", "ADA"))); + }); + } + + @Test + @DisplayName("Should update with LEFT function in where clause") + void shouldUpdateWhereFunctionLeft() { + template.update(Person.class) + .set("name").to("Ada") + .where(Function.left("name", 2)).eq("Ad") + .execute(); + + Mockito.verify(managerMock).update(captor.capture()); + var update = captor.getValue(); + + SoftAssertions.assertSoftly(soft -> { + var condition = update.where().orElseThrow(); + soft.assertThat(condition).as("condition matches LEFT(name, 2) = Ad") + .isEqualTo(CriteriaCondition.eq(Element.of("LEFT(name, 2)", "Ad"))); + }); + } + + @Test + @DisplayName("Should update with LOWER function in AND condition") + void shouldUpdateAndFunctionLower() { + template.update(Person.class) + .set("name").to("Ada") + .where("age").gt(10).and(Function.lower("name")).eq("ada") + .execute(); + + Mockito.verify(managerMock).update(captor.capture()); + var update = captor.getValue(); + + SoftAssertions.assertSoftly(soft -> { + var condition = update.where().orElseThrow(); + soft.assertThat(condition.condition()).as("condition is AND").isEqualTo(Condition.AND); + }); + } + }