diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index bc4af6504..172fac779 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -34,6 +34,9 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version - Remove Apache Tinkerpop from the project and move it as a driver +=== Changed + +- by default disable Cursor pagination in the `SemiStructuredTemplate` when there is more than one sort == [1.1.1] - 2023-05-25 diff --git a/README.adoc b/README.adoc index 72465bd51..4bd206f98 100644 --- a/README.adoc +++ b/README.adoc @@ -266,8 +266,6 @@ private Queue orders; private Map map; ---- - - === Column Family Jakarta NoSQL provides a Column Family template to explore the specific behavior of this NoSQL type. diff --git a/jnosql-communication/jnosql-communication-core/src/main/java/org/eclipse/jnosql/communication/Configurations.java b/jnosql-communication/jnosql-communication-core/src/main/java/org/eclipse/jnosql/communication/Configurations.java index c9581fa89..c09db52dc 100644 --- a/jnosql-communication/jnosql-communication-core/src/main/java/org/eclipse/jnosql/communication/Configurations.java +++ b/jnosql-communication/jnosql-communication-core/src/main/java/org/eclipse/jnosql/communication/Configurations.java @@ -19,27 +19,65 @@ import java.util.function.Supplier; /** - * This enum contains all the commons configurations that might be used to the NoSQL databases. - * It implements {@link Supplier} which returns the property value on the arrangement. + * This enum contains common configurations that are frequently used for NoSQL databases. + * It provides a standardized way to retrieve configuration keys using the {@link Supplier} interface, + * which allows these properties to be fetched dynamically in different contexts. + * + *

Each constant in this enum represents a specific configuration option, such as connection details + * (user, password, host) or other database-related settings (like encryption and pagination behavior). + * By implementing {@link Supplier}, each enum constant can supply the associated property value directly + * via the {@link #get()} method.

+ * + *

Developers can reference these constants throughout the application to avoid hardcoding configuration + * keys and ensure consistent access to NoSQL database properties. This is particularly useful for managing + * complex or large-scale database configurations where multiple properties (e.g., hosts, pagination settings) + * are involved.

*/ public enum Configurations implements Supplier { + /** - * to set a user in a NoSQL database + * Configuration for setting the user in a NoSQL database. + *

This property is used to specify the username required for authenticating + * the connection to the NoSQL database.

+ *

Example: jakarta.nosql.user=admin

*/ USER("jakarta.nosql.user"), + /** - * to set a password in a database + * Configuration for setting the password in a NoSQL database. + *

This property is used in conjunction with the {@link #USER} configuration + * to authenticate the database connection by providing the user’s password.

+ *

Example: jakarta.nosql.password=secret

*/ PASSWORD("jakarta.nosql.password"), + /** - * the host configuration that might have more than one with a number as a suffix, - * such as jakarta.nosql.host-1=localhost, jakarta.nosql.host-2=host2 + * Host configuration for connecting to the NoSQL database. + *

This property allows setting multiple hosts by using a numbered suffix (e.g., host-1, host-2). + * This is useful for distributed databases or in cases where multiple instances or replicas + * of the database are running.

+ *

Example: jakarta.nosql.host-1=localhost, jakarta.nosql.host-2=remote-host

*/ HOST("jakarta.nosql.host"), + + /** + * Configuration to enable encryption settings for NoSQL database connections. + *

This property is used to configure encryption settings, which are critical + * for securing data in transit or at rest within the NoSQL database. It defines the encryption + * protocols or keys that should be applied.

+ *

Example: jakarta.nosql.settings.encryption=AES256

+ */ + ENCRYPTION("jakarta.nosql.settings.encryption"), + /** - * A configuration to set the encryption to settings property. + * Configuration to enable cursor-based pagination with multiple sorting in NoSQL databases. + *

This property allows enabling cursor pagination with support for multiple sorting fields. + * By default, multiple sorting is disabled due to potential inconsistencies in NoSQL databases when + * sorting by multiple fields that contain duplicate values. Enabling this option allows users to + * explicitly manage multi-field sorting during cursor-based pagination.

+ *

To enable, set: org.eclipse.jnosql.pagination.cursor=true

*/ - ENCRYPTION("jakarta.nosql.settings.encryption"); + CURSOR_PAGINATION_MULTIPLE_SORTING("org.eclipse.jnosql.pagination.cursor"); private final String configuration; @@ -52,4 +90,4 @@ public enum Configurations implements Supplier { public String get() { return configuration; } -} \ No newline at end of file +} 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 872949bbe..49b40632a 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 @@ -30,6 +30,7 @@ import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.IdNotFoundException; import org.eclipse.jnosql.mapping.core.NoSQLPage; +import org.eclipse.jnosql.mapping.core.config.MicroProfileSettings; import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; import org.eclipse.jnosql.mapping.metadata.FieldMetadata; @@ -44,11 +45,13 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.UnaryOperator; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static java.util.Objects.requireNonNull; +import static org.eclipse.jnosql.communication.Configurations.CURSOR_PAGINATION_MULTIPLE_SORTING; /** * An abstract implementation of the {@link SemiStructuredTemplate} interface providing @@ -59,6 +62,8 @@ */ public abstract class AbstractSemiStructuredTemplate implements SemiStructuredTemplate { + private static final Logger LOGGER = Logger.getLogger(AbstractSemiStructuredTemplate.class.getName()); + private static final QueryParser PARSER = new QueryParser(); /** @@ -327,6 +332,15 @@ public void deleteAll(Class type) { public CursoredPage selectCursor(SelectQuery query, PageRequest pageRequest){ Objects.requireNonNull(query, "query is required"); Objects.requireNonNull(pageRequest, "pageRequest is required"); + LOGGER.finest(() -> "Executing query: " + query); + var enableMultipleSorting = MicroProfileSettings.INSTANCE.get(CURSOR_PAGINATION_MULTIPLE_SORTING, Boolean.class) + .orElse(false); + LOGGER.finest(() -> "Cursor pagination with multiple sorting is enabled: " + enableMultipleSorting); + + if (!enableMultipleSorting && query.sorts().size() > 1) { + throw new UnsupportedOperationException("Cursor pagination with multiple sorting is not supported, " + + "enable it by setting the property " + CURSOR_PAGINATION_MULTIPLE_SORTING.get() + " to true"); + } CursoredPage cursoredPage = this.manager().selectCursor(query, pageRequest); List entities = cursoredPage.stream().map(c -> converter().toEntity(c)).toList(); PageRequest nextPageRequest = cursoredPage.hasNext()? cursoredPage.nextPageRequest() : null; diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredTemplate.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredTemplate.java index 66eafa98a..8a79019a8 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredTemplate.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/SemiStructuredTemplate.java @@ -217,6 +217,14 @@ public interface SemiStructuredTemplate extends Template { *

For cursor-based pagination, at least one sort field must be specified in the {@link SelectQuery} order clause; otherwise, an * {@link IllegalArgumentException} will be thrown.

* + *

By default, multiple sorting is disabled due to the behavior of NoSQL databases. In NoSQL systems, sorting by multiple fields can result + * in unpredictable or inconsistent results, particularly when those fields contain duplicate values. Relational databases are more deterministic + * in their sorting algorithms, but NoSQL systems such as MongoDB may return results in varying order if there is no unique field, such as `_id`, + * to break ties. This behavior makes it difficult to guarantee stable pagination across requests.

+ * + *

To enable multiple sorting, set the property org.eclipse.jnosql.pagination.cursor=true. For more details, refer to + * {@link org.eclipse.jnosql.communication.Configurations#CURSOR_PAGINATION_MULTIPLE_SORTING}.

+ * * @param query the query to retrieve entities * @param pageRequest the page request defining the cursor-based paging * @param the entity type diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplateTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplateTest.java index fc9907d8b..50fb1a3b8 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplateTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/DefaultSemiStructuredTemplateTest.java @@ -21,6 +21,7 @@ import jakarta.data.page.impl.CursoredPageRecord; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import org.eclipse.jnosql.communication.Configurations; import org.eclipse.jnosql.mapping.PreparedStatement; import org.assertj.core.api.SoftAssertions; import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; @@ -517,6 +518,41 @@ void shouldSelectCursor() { } + @Test + void shouldThrowExceptionWhenCursorHasMultipleSorts() { + System.setProperty(Configurations.CURSOR_PAGINATION_MULTIPLE_SORTING.get(), "true"); + PageRequest request = PageRequest.ofSize(2); + + PageRequest afterKey = PageRequest.afterCursor(PageRequest.Cursor.forKey("Ada"), 1, 2, false); + SelectQuery query = select().from("Person").orderBy("name").asc().orderBy("age").desc().build(); + + Mockito.when(managerMock.selectCursor(query, request)) + .thenReturn(new CursoredPageRecord<>(content(), + Collections.emptyList(), -1, request, afterKey, null)); + + PageRequest personRequest = PageRequest.ofSize(2); + + CursoredPage result = template.selectCursor(query, personRequest); + org.assertj.core.api.Assertions.assertThat(result).isNotNull(); + System.clearProperty(Configurations.CURSOR_PAGINATION_MULTIPLE_SORTING.get()); + } + + @Test + void shouldExecuteMultipleSortsWhenEnableIt() { + PageRequest request = PageRequest.ofSize(2); + + PageRequest afterKey = PageRequest.afterCursor(PageRequest.Cursor.forKey("Ada"), 1, 2, false); + SelectQuery query = select().from("Person").orderBy("name").asc().orderBy("age").desc().build(); + + Mockito.when(managerMock.selectCursor(query, request)) + .thenReturn(new CursoredPageRecord<>(content(), + Collections.emptyList(), -1, request, afterKey, null)); + + PageRequest personRequest = PageRequest.ofSize(2); + org.assertj.core.api.Assertions.assertThatThrownBy(() -> template.selectCursor(query, personRequest)) + .isInstanceOf(UnsupportedOperationException.class); + } + @Test void shouldSelectOffSet() { PageRequest request = PageRequest.ofPage(2).size(10);