Skip to content

Commit

Permalink
Merge pull request #554 from eclipse/disable-cursor-plus-elements
Browse files Browse the repository at this point in the history
Disable cursor plus elements
  • Loading branch information
otaviojava authored Sep 12, 2024
2 parents 9499786 + d23886b commit ddc2715
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 0 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,6 @@ private Queue<String> orders;
private Map<String, String> map;
----



=== Column Family

Jakarta NoSQL provides a Column Family template to explore the specific behavior of this NoSQL type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* <p>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.</p>
*/
public enum Configurations implements Supplier<String> {

/**
* to set a user in a NoSQL database
* Configuration for setting the user in a NoSQL database.
* <p>This property is used to specify the username required for authenticating
* the connection to the NoSQL database.</p>
* <p>Example: <code>jakarta.nosql.user=admin</code></p>
*/
USER("jakarta.nosql.user"),

/**
* to set a password in a database
* Configuration for setting the password in a NoSQL database.
* <p>This property is used in conjunction with the {@link #USER} configuration
* to authenticate the database connection by providing the user’s password.</p>
* <p>Example: <code>jakarta.nosql.password=secret</code></p>
*/
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.
* <p>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.</p>
* <p>Example: <code>jakarta.nosql.host-1=localhost</code>, <code>jakarta.nosql.host-2=remote-host</code></p>
*/
HOST("jakarta.nosql.host"),

/**
* Configuration to enable encryption settings for NoSQL database connections.
* <p>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.</p>
* <p>Example: <code>jakarta.nosql.settings.encryption=AES256</code></p>
*/
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.
* <p>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.</p>
* <p>To enable, set: <code>org.eclipse.jnosql.pagination.cursor=true</code></p>
*/
ENCRYPTION("jakarta.nosql.settings.encryption");
CURSOR_PAGINATION_MULTIPLE_SORTING("org.eclipse.jnosql.pagination.cursor");

private final String configuration;

Expand All @@ -52,4 +90,4 @@ public enum Configurations implements Supplier<String> {
public String get() {
return configuration;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();

/**
Expand Down Expand Up @@ -327,6 +332,15 @@ public <T> void deleteAll(Class<T> type) {
public <T> CursoredPage<T> 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<CommunicationEntity> cursoredPage = this.manager().selectCursor(query, pageRequest);
List<T> entities = cursoredPage.stream().<T>map(c -> converter().toEntity(c)).toList();
PageRequest nextPageRequest = cursoredPage.hasNext()? cursoredPage.nextPageRequest() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ public interface SemiStructuredTemplate extends Template {
* <p>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.</p>
*
* <p>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.</p>
*
* <p>To enable multiple sorting, set the property <b>org.eclipse.jnosql.pagination.cursor=true</b>. For more details, refer to
* {@link org.eclipse.jnosql.communication.Configurations#CURSOR_PAGINATION_MULTIPLE_SORTING}.</p>
*
* @param query the query to retrieve entities
* @param pageRequest the page request defining the cursor-based paging
* @param <T> the entity type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Person> 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);
Expand Down

0 comments on commit ddc2715

Please sign in to comment.