Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable cursor plus elements #554

Merged
merged 12 commits into from
Sep 12, 2024
Merged
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