diff --git a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/UpdateProvider.java b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/UpdateProvider.java index f666e322a..93d491b72 100644 --- a/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/UpdateProvider.java +++ b/jnosql-communication/jnosql-communication-query/src/main/java/org/eclipse/jnosql/communication/query/data/UpdateProvider.java @@ -32,7 +32,7 @@ */ public final class UpdateProvider extends AbstractWhere implements Function { - private List items = new ArrayList<>(); + private final List items = new ArrayList<>(); @Override public UpdateQuery apply(String query) { diff --git a/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryParser.java b/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryParser.java index fb0212f79..6114ddff4 100644 --- a/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryParser.java +++ b/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryParser.java @@ -38,10 +38,10 @@ public final class QueryParser { */ public Stream query(String query, String entity, DatabaseManager manager, CommunicationObserverParser observer) { validation(query, manager, observer); - String command = extractQueryCommand(query); + var command = QueryType.parse(query); return switch (command) { - case "DELETE" -> delete.query(query, manager, observer); - case "UPDATE" -> update.query(query, manager, observer); + case DELETE -> delete.query(query, manager, observer); + case UPDATE -> update.query(query, manager, observer); default -> select.query(query, entity, manager, observer); }; } @@ -60,21 +60,14 @@ public Stream query(String query, String entity, DatabaseMa */ public CommunicationPreparedStatement prepare(String query, String entity, DatabaseManager manager, CommunicationObserverParser observer) { validation(query, manager, observer); - String command = extractQueryCommand(query); + var command = QueryType.parse(query); return switch (command) { - case "DELETE" -> delete.prepare(query, manager, observer); - case "UPDATE" -> update.prepare(query, manager, observer); + case DELETE -> delete.prepare(query, manager, observer); + case UPDATE -> update.prepare(query, manager, observer); default -> select.prepare(query, entity, manager, observer); }; } - private String extractQueryCommand(String query){ - if(query.length() < 6){ - return ""; - } - return query.substring(0, 6).toUpperCase(); - } - private void validation(String query, DatabaseManager manager, CommunicationObserverParser observer) { Objects.requireNonNull(query, "query is required"); Objects.requireNonNull(manager, "manager is required"); diff --git a/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryType.java b/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryType.java new file mode 100644 index 000000000..69506eb13 --- /dev/null +++ b/jnosql-communication/jnosql-communication-semistructured/src/main/java/org/eclipse/jnosql/communication/semistructured/QueryType.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 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. + * + */ +package org.eclipse.jnosql.communication.semistructured; + +import java.util.Objects; + +/** + * Enum representing the different types of queries supported in Jakarta Data. + * + *

The {@code QueryType} enum categorizes queries into three main types: {@code SELECT}, + * {@code DELETE}, and {@code UPDATE}. These types correspond to the standard operations + * typically executed against a database. This enum is used to interpret and classify + * queries within the Jakarta Data API, particularly in implementations like Eclipse JNoSQL.

+ * + *
    + *
  • {@link #SELECT} - Represents a query that retrieves data from the database.
  • + *
  • {@link #DELETE} - Represents a query that removes data from the database.
  • + *
  • {@link #UPDATE} - Represents a query that modifies existing data in the database.
  • + *
+ * + *

The {@link #parse(String)} method is provided to determine the type of a given query + * string by extracting and evaluating its command keyword. The method returns a corresponding + * {@code QueryType} based on the first six characters of the query, assuming that the query + * begins with a standard SQL-like command.

+ * + *

Note that if the query string does not contain a recognizable command (e.g., if it is + * shorter than six characters or does not match any known command), the method defaults to + * {@code SELECT}.

+ * + *

This enum is particularly relevant for NoSQL implementations like Eclipse JNoSQL, where + * the query language might differ from traditional SQL, yet still, adhere to the concepts + * of selection, deletion, and updating of data.

+ */ +public enum QueryType { + + /** + * Represents a query that retrieves data from the database. + * This is the default query type when no specific command is recognized. + */ + SELECT, + + /** + * Represents a query that removes data from the database. + * Typically used to delete one or more records based on certain conditions. + */ + DELETE, + + /** + * Represents a query that modifies existing data in the database. + * Typically used to update one or more records based on certain conditions. + */ + UPDATE; + + /** + * Parses the given query string to determine the type of query. + * + * @param query the query string to parse + * @return the {@code QueryType} corresponding to the query command + */ + public static QueryType parse(String query) { + Objects.requireNonNull(query, "Query string cannot be null"); + String command = QueryType.extractQueryCommand(query); + return switch (command) { + case "DELETE" -> DELETE; + case "UPDATE" -> UPDATE; + default -> SELECT; + }; + } + + /** + * Checks if the current {@code QueryType} is not a {@code SELECT} operation. + * This method is useful for determining whether the query is intended to modify data + * (i.e., either a {@code DELETE} or {@code UPDATE} operation) rather than retrieve it. + * It can be employed in scenarios where different logic is applied based on whether + * a query modifies data. For example, {@code if (queryType.isNotSelect())} can be used + * to trigger actions specific to non-SELECT queries. This method returns {@code true} + * if the current {@code QueryType} is either {@code DELETE} or {@code UPDATE}, + * and {@code false} if it is {@code SELECT}. + */ + public boolean isNotSelect() { + return this != SELECT; + } + + /** + * Validates the return type of method based on the type of query being executed. + *

+ * This method checks whether the specified query is a {@code DELETE} or {@code UPDATE} operation + * and ensures that the return type is {@code Void}. If the query is not a {@code SELECT} operation + * and the return type is not {@code Void}, an {@code UnsupportedOperationException} is thrown. + *

+ * This validation is necessary because {@code DELETE} and {@code UPDATE} operations typically + * do not return a result set, and as such, they should have a {@code Void} return type. + * + * @param returnType the return type of the method executing the query + * @param query the query being executed + * @throws UnsupportedOperationException if the query is a {@code DELETE} or {@code UPDATE} operation + * and the return type is not {@code Void} + */ + public void checkValidReturn(Class returnType, String query) { + if (isNotSelect() && !isVoid(returnType)) { + throw new UnsupportedOperationException("The return type must be Void when the query is not a SELECT operation, due to the nature" + + " of DELETE and UPDATE operations. The query: " + query); + } + } + + private boolean isVoid(Class returnType) { + return returnType == Void.class || returnType == Void.TYPE; + } + + private static String extractQueryCommand(String query){ + if(query.length() < 6){ + return ""; + } + return query.substring(0, 6).toUpperCase(); + } +} diff --git a/jnosql-communication/jnosql-communication-semistructured/src/test/java/org/eclipse/jnosql/communication/semistructured/QueryTypeTest.java b/jnosql-communication/jnosql-communication-semistructured/src/test/java/org/eclipse/jnosql/communication/semistructured/QueryTypeTest.java new file mode 100644 index 000000000..9319335c5 --- /dev/null +++ b/jnosql-communication/jnosql-communication-semistructured/src/test/java/org/eclipse/jnosql/communication/semistructured/QueryTypeTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 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. + * + */ +package org.eclipse.jnosql.communication.semistructured; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class QueryTypeTest { + + + @Test + void shouldParseSelectQuery() { + String query = "SELECT * FROM table"; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.SELECT); + } + + @Test + void shouldParseDeleteQuery() { + String query = "DELETE FROM table WHERE id = 1"; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.DELETE); + } + + @Test + void shouldParseUpdateQuery() { + String query = "UPDATE table SET name = 'newName' WHERE id = 1"; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.UPDATE); + } + + @Test + void shouldDefaultToSelectForUnknownQuery() { + String query = "INSERT INTO table (id, name) VALUES (1, 'name')"; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.SELECT); + } + + @Test + void shouldDefaultToSelectForShortQuery() { + String query = "DELE"; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.SELECT); + } + + @Test + void shouldDefaultToSelectForEmptyQuery() { + String query = ""; + QueryType result = QueryType.parse(query); + assertThat(result).isEqualTo(QueryType.SELECT); + } + + @Test + void shouldThrowNullPointerExceptionForNullQuery() { + String query = null; + assertThatThrownBy(() -> QueryType.parse(query)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void shouldReturnIsNotSelect() { + Assertions.assertThat(QueryType.SELECT.isNotSelect()).isFalse(); + Assertions.assertThat(QueryType.DELETE.isNotSelect()).isTrue(); + Assertions.assertThat(QueryType.UPDATE.isNotSelect()).isTrue(); + } + + @Test + void shouldCheckValidReturn() { + QueryType.SELECT.checkValidReturn(String.class, "SELECT * FROM table"); + QueryType.DELETE.checkValidReturn(Void.class, "DELETE FROM table WHERE id = 1"); + QueryType.UPDATE.checkValidReturn(Void.class, "UPDATE table SET name = 'newName' WHERE id = 1"); + assertThatThrownBy(() -> QueryType.DELETE.checkValidReturn(String.class, "DELETE FROM table WHERE id = 1")) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> QueryType.UPDATE.checkValidReturn(String.class, "UPDATE table SET name = 'newName' WHERE id = 1")) + .isInstanceOf(UnsupportedOperationException.class); + } +} \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/DynamicReturnConverter.java b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/DynamicReturnConverter.java index 162463eb4..d2f014cb7 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/DynamicReturnConverter.java +++ b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/DynamicReturnConverter.java @@ -66,7 +66,7 @@ public Object convert(DynamicReturn dynamic) { * * @return the result from the query annotation */ - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"unchecked"}) public Object convert(DynamicQueryMethodReturn dynamicQueryMethod) { Method method = dynamicQueryMethod.method(); Object[] args = dynamicQueryMethod.args(); diff --git a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java index 25f393f7f..fa270a092 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java +++ b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java @@ -141,10 +141,12 @@ static SpecialParameters of(Object[] parameters, Function sortPa Arrays.stream(sortArray).map(s -> mapper(s, sortParser)).forEach(sorts::add); } else if (parameter instanceof PageRequest request) { pageRequest = request; - } else if (parameter instanceof Iterable iterable) { - for (Object value : iterable) { - if (value instanceof Sort sortValue) { - sorts.add(mapper(sortValue, sortParser)); + }else { + if (parameter instanceof Iterable iterable) { + for (Object value : iterable) { + if (value instanceof Sort sortValue) { + sorts.add(mapper(sortValue, sortParser)); + } } } } @@ -162,7 +164,8 @@ public static boolean isSpecialParameter(Object parameter) { return parameter instanceof Sort || parameter instanceof Limit || parameter instanceof Order - || parameter instanceof PageRequest; + || parameter instanceof PageRequest + || parameter instanceof Sort[]; } /** @@ -185,7 +188,8 @@ public static boolean isSpecialParameter(Class parameter) { return Sort.class.isAssignableFrom(parameter) || Limit.class.isAssignableFrom(parameter) || Order.class.isAssignableFrom(parameter) - || PageRequest.class.isAssignableFrom(parameter); + || PageRequest.class.isAssignableFrom(parameter) + || parameter.isArray() && Sort.class.isAssignableFrom(parameter.getComponentType()); } /** diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java index d03c8ded8..a07155931 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java @@ -19,6 +19,7 @@ import jakarta.data.repository.OrderBy; import jakarta.data.repository.Query; import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.QueryType; import org.eclipse.jnosql.mapping.core.repository.DynamicQueryMethodReturn; import org.eclipse.jnosql.mapping.core.repository.DynamicReturn; import org.eclipse.jnosql.mapping.core.repository.RepositoryReflectionUtils; @@ -28,6 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.logging.Logger; import java.util.stream.Stream; /** @@ -38,11 +40,20 @@ */ public abstract class AbstractSemiStructuredRepositoryProxy extends BaseSemiStructuredRepository { + private static final Logger LOGGER = Logger.getLogger(AbstractSemiStructuredRepositoryProxy.class.getName()); + @Override protected Object executeQuery(Object instance, Method method, Object[] params) { + LOGGER.finest("Executing query on method: " + method); Class type = entityMetadata().type(); var entity = entityMetadata().name(); var pageRequest = DynamicReturn.findPageRequest(params); + var queryValue = method.getAnnotation(Query.class).value(); + var queryType = QueryType.parse(queryValue); + var returnType = method.getReturnType(); + LOGGER.finest("Query: " + queryValue + " with type: " + queryType + " and return type: " + returnType); + queryType.checkValidReturn(returnType, queryValue); + var methodReturn = DynamicQueryMethodReturn.builder() .args(params) .method(method) diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandler.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandler.java index 727ba495f..5b54ab260 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandler.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandler.java @@ -19,6 +19,7 @@ import jakarta.data.page.Page; import jakarta.data.repository.Query; import jakarta.enterprise.inject.spi.CDI; +import org.eclipse.jnosql.communication.semistructured.QueryType; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.core.query.AbstractRepository; import org.eclipse.jnosql.mapping.core.query.AnnotationOperation; @@ -127,16 +128,22 @@ public Object invoke(Object instance, Method method, Object[] params) throws Thr var repositoryMetadata = repositoryMetadata(method); if (repositoryMetadata.metadata().isEmpty()) { var query = method.getAnnotation(Query.class); + var queryType = QueryType.parse(query.value()); + var returnType = method.getReturnType(); + LOGGER.fine("Executing the query " + query.value() + " with the type " + queryType + " and the return type " + returnType); + queryType.checkValidReturn(returnType, query.value()); Map parameters = RepositoryReflectionUtils.INSTANCE.getParams(method, params); + LOGGER.fine("Parameters: " + parameters); var prepare = template.prepare(query.value()); parameters.forEach(prepare::bind); if (prepare.isCount()) { return prepare.count(); } Stream entities = prepare.result(); - if (method.getReturnType().equals(long.class) || method.getReturnType().equals(Long.class)) { + if(isLong(method)) { return entities.count(); } + return Void.class; } return unwrapInvocationTargetException(() -> repository(method).invoke(instance, method, params)); @@ -270,5 +277,9 @@ private Class getGenericTypeFromParameter(Parameter parameter) { throw new IllegalArgumentException("Cannot determine generic type from parameter"); } + private static boolean isLong(Method method) { + return method.getReturnType().equals(long.class) || method.getReturnType().equals(Long.class); + } + } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerTest.java index f277666f7..27cfae466 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerTest.java @@ -436,4 +436,24 @@ void shouldExecuteExistBy() { }); } + + @Test + void shouldReturnNotSupportedWhenQueryIsNotSelectAsDelete() { + var preparedStatement = Mockito.mock(org.eclipse.jnosql.mapping.semistructured.PreparedStatement.class); + Mockito.when(template.prepare(Mockito.anyString())).thenReturn(preparedStatement); + Mockito.when(template.query(Mockito.anyString())) + .thenReturn(Stream.of(Person.builder().age(26).name("Ada").build())); + Assertions.assertThatThrownBy(() ->people.deleteByNameReturnInt()) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void shouldReturnNotSupportedWhenQueryIsNotSelectAsUpdate() { + var preparedStatement = Mockito.mock(org.eclipse.jnosql.mapping.semistructured.PreparedStatement.class); + Mockito.when(template.prepare(Mockito.anyString())).thenReturn(preparedStatement); + Mockito.when(template.query(Mockito.anyString())) + .thenReturn(Stream.of(Person.builder().age(26).name("Ada").build())); + Assertions.assertThatThrownBy(() ->people.updateReturnInt()) + .isInstanceOf(UnsupportedOperationException.class); + } } \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/People.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/People.java index 4a01f1a3a..8ce126ebb 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/People.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/People.java @@ -97,4 +97,10 @@ public interface People { default String defaultMethod() { return "default"; } + + @Query("delete from Person where name = :name") + long deleteByNameReturnInt(); + + @Query("update Person where name = :name") + long updateReturnInt(); }