findByLastName(String lastName);
+
+ @Query(expression = "$.age > ?0")
+ List findByAgeGreaterThan(int age);
+
+ @Query(expression = "$.isActive.get(type: BOOL) == ?0")
+ List
findByIsActive(boolean isActive);
+}
+----
+
+== Custom Queries
+
+Just like with regular and metadata queries, you can use <> with DSL expressions when such flexibility is required or when there is no need to annotate repository methods.
+
+[source,java]
+----
+ // creating an expression "firsName is equal to John" using DSL expression
+ Qualifier firstNameEqJohn = Qualifier.dslStringBuilder()
+ .setDSLString("$.firstName == 'John'")
+ .build();
+ result = repository.findUsingQuery(new Query(firstNameEqJohn))
+ assertThat(result).containsOnly(john);
+----
+
diff --git a/src/main/asciidoc/reference/query-methods-collection.adoc b/src/main/asciidoc/reference/query-methods-collection.adoc
index 81fae6713..ed31c977f 100644
--- a/src/main/asciidoc/reference/query-methods-collection.adoc
+++ b/src/main/asciidoc/reference/query-methods-collection.adoc
@@ -121,7 +121,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#list[informa
----
findByStringListBetween(Collection lowerLimit, Collection upperLimit)
----
-|...where x.stringList between ? and ?
+|...where x.stringList between ? (inclusive) and ? (exclusive)
|only scan
|Find records where `stringList` bin value is in the range between the given arguments.
See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#list[information about ordering].
diff --git a/src/main/asciidoc/reference/query-methods-map.adoc b/src/main/asciidoc/reference/query-methods-map.adoc
index 895382e2d..b272680fa 100644
--- a/src/main/asciidoc/reference/query-methods-map.adoc
+++ b/src/main/asciidoc/reference/query-methods-map.adoc
@@ -119,7 +119,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[informat
----
findByStringMapBetween(Map lowerLimit, Map upperLimit)
----
-|...where x.stringMap between ? and ?
+|...where x.stringMap between ? (inclusive) and ? (exclusive)
|only scan
|Find records where `stringMap` bin value is in the range between the given arguments.
See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[information about ordering].
diff --git a/src/main/asciidoc/reference/query-methods-modification.adoc b/src/main/asciidoc/reference/query-methods-modification.adoc
index 276021f0e..a080d9991 100644
--- a/src/main/asciidoc/reference/query-methods-modification.adoc
+++ b/src/main/asciidoc/reference/query-methods-modification.adoc
@@ -36,13 +36,15 @@ post-processing step. This limiting can affect performance, depending on the num
results and limiting parameters.
[[find-using-query]]
-== Find Using Query
+== Custom Queries
-User can perform a custom `Query` for finding matching entities in the Aerospike database.
-A `Query` can be created using a `Qualifier` which represents an expression.
+You can perform a custom `Query` for finding matching entities in the Aerospike database via `findUsingQuery()` method.
+
+A `Query` object can be created using a `Qualifier` which represents an expression.
It may contain other qualifiers and combine them using either `AND` or `OR`.
-`Qualifier` can be created for regular bins, metadata and ids (primary keys).
+`Qualifier` can be created for regular bins, metadata and ids (primary keys). It can also be created for a DSL expression.
+
Below is an example of different variations:
[source,java]
@@ -73,4 +75,11 @@ Below is an example of different variations:
// expressions are combined using AND
result = repository.findUsingQuery(new Query(Qualifier.and(firstNameEqJohn, keyEqJohnsId, sinceUpdateTimeLt50Seconds)));
assertThat(result).containsOnly(john);
+
+ // creating an expression "firsName is equal to John" using DSL expression
+ Qualifier firstNameEqJohn = Qualifier.dslStringBuilder()
+ .setDSLString("$.firstName == 'John'")
+ .build();
+ result = repository.findUsingQuery(new Query(firstNameEqJohn))
+ assertThat(result).containsOnly(john);
----
\ No newline at end of file
diff --git a/src/main/asciidoc/reference/query-methods-pojo.adoc b/src/main/asciidoc/reference/query-methods-pojo.adoc
index 0b2143c34..a8c00e715 100644
--- a/src/main/asciidoc/reference/query-methods-pojo.adoc
+++ b/src/main/asciidoc/reference/query-methods-pojo.adoc
@@ -120,7 +120,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[informat
----
findByAddressBetween(Address lowerLimit, Address upperLimit)
----
-|...where x.address between ? and ?
+|...where x.address between ? (inclusive) and ? (exclusive)
|only scan
|Find records where `address` bin value (POJOs are stored in AerospikeDB as maps) is in the range between
the given arguments. See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[information about ordering].
diff --git a/src/main/asciidoc/reference/query-methods-preface.adoc b/src/main/asciidoc/reference/query-methods-preface.adoc
index 3e775e763..1b0b1fd73 100644
--- a/src/main/asciidoc/reference/query-methods-preface.adoc
+++ b/src/main/asciidoc/reference/query-methods-preface.adoc
@@ -1,7 +1,7 @@
[[aerospike.query-methods-preface]]
= Query Methods
-Spring Data Aerospike supports defining queries by method name in the Repository interface so that the implementation
+Spring Data Aerospike supports defining queries by method names in the Repository interface so that the implementation
is generated.
The format of method names is fairly flexible, comprising a verb and criteria.
@@ -19,7 +19,8 @@ Repository queries in Spring Data Aerospike can be divided into 3 groups:
* Queries that only use <>
-* Queries that use both secondary index and scan (when looking for a nested integer or string property).
+* Queries that use both secondary index and scan (e.g., for indexed queries combined with AND
+or when looking for a nested integer or string property).
If no corresponding secondary index is found, each query does a fallback to using scan only.
diff --git a/src/main/asciidoc/reference/query-methods-simple-property.adoc b/src/main/asciidoc/reference/query-methods-simple-property.adoc
index 327eec036..0e93b6451 100644
--- a/src/main/asciidoc/reference/query-methods-simple-property.adoc
+++ b/src/main/asciidoc/reference/query-methods-simple-property.adoc
@@ -155,9 +155,9 @@ findByAgeBetween(int lowerLimit, int upperLimit)
findByFirstNameBetween(String lowerLimit, String upperLimit)
----
-|...where x.age between ? and ?
+|...where x.age between ? (inclusive) and ? (exclusive)
-...where x.firstName between ? and ?
+...where x.firstName between ? (inclusive) and ? (exclusive)
|only for integers
|Strings are compared by order of each byte, assuming they have UTF-8 encoding.
See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#string[information about ordering].
diff --git a/src/main/java/org/springframework/data/aerospike/annotation/Query.java b/src/main/java/org/springframework/data/aerospike/annotation/Query.java
new file mode 100644
index 000000000..8b9f7f1c5
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/annotation/Query.java
@@ -0,0 +1,29 @@
+package org.springframework.data.aerospike.annotation;
+
+import org.springframework.data.annotation.QueryAnnotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Query annotation to define expressions for repository methods. Allows using either a fixed DSL String or
+ * a String with placeholders like {@code ?0}, {@code ?1} etc.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
+@Documented
+@QueryAnnotation
+@Beta
+public @interface Query {
+
+ /**
+ * Use expression DSL to define the query taking precedence over method name
+ *
+ * @return empty {@link String} by default.
+ */
+ String expression();
+}
diff --git a/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java b/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java
index 486246cd0..d09c48db6 100644
--- a/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java
+++ b/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java
@@ -16,6 +16,7 @@
package org.springframework.data.aerospike.config;
import com.aerospike.client.IAerospikeClient;
+import com.aerospike.dsl.DSLParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
@@ -31,6 +32,7 @@
import org.springframework.data.aerospike.query.StatementBuilder;
import org.springframework.data.aerospike.query.cache.IndexInfoParser;
import org.springframework.data.aerospike.query.cache.IndexRefresher;
+import org.springframework.data.aerospike.query.cache.IndexesCacheHolder;
import org.springframework.data.aerospike.query.cache.IndexesCacheUpdater;
import org.springframework.data.aerospike.query.cache.InternalIndexOperations;
import org.springframework.data.aerospike.server.version.ServerVersionSupport;
@@ -45,12 +47,13 @@ public AerospikeTemplate aerospikeTemplate(IAerospikeClient aerospikeClient,
AerospikeMappingContext aerospikeMappingContext,
AerospikeExceptionTranslator aerospikeExceptionTranslator,
QueryEngine queryEngine, IndexRefresher indexRefresher,
+ IndexesCacheHolder indexCacheHolder,
ServerVersionSupport serverVersionSupport,
- AerospikeSettings settings)
+ AerospikeSettings settings, DSLParser dslParser)
{
return new AerospikeTemplate(aerospikeClient, settings.getDataSettings().getNamespace(),
mappingAerospikeConverter, aerospikeMappingContext, aerospikeExceptionTranslator, queryEngine,
- indexRefresher, serverVersionSupport);
+ indexRefresher, indexCacheHolder, serverVersionSupport, dslParser);
}
@Bean(name = "aerospikeQueryEngine")
diff --git a/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java b/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java
index e4ae934c5..1afbc9c82 100644
--- a/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java
+++ b/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java
@@ -19,6 +19,7 @@
import com.aerospike.client.policy.ClientPolicy;
import com.aerospike.client.reactor.AerospikeReactorClient;
import com.aerospike.client.reactor.IAerospikeReactorClient;
+import com.aerospike.dsl.DSLParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
@@ -33,6 +34,7 @@
import org.springframework.data.aerospike.query.ReactorQueryEngine;
import org.springframework.data.aerospike.query.StatementBuilder;
import org.springframework.data.aerospike.query.cache.IndexInfoParser;
+import org.springframework.data.aerospike.query.cache.IndexesCacheHolder;
import org.springframework.data.aerospike.query.cache.IndexesCacheUpdater;
import org.springframework.data.aerospike.query.cache.InternalIndexOperations;
import org.springframework.data.aerospike.query.cache.ReactorIndexRefresher;
@@ -54,12 +56,13 @@ public ReactiveAerospikeTemplate reactiveAerospikeTemplate(MappingAerospikeConve
IAerospikeReactorClient aerospikeReactorClient,
ReactorQueryEngine reactorQueryEngine,
ReactorIndexRefresher reactorIndexRefresher,
+ IndexesCacheHolder indexCacheHolder,
ServerVersionSupport serverVersionSupport,
- AerospikeSettings settings)
+ AerospikeSettings settings, DSLParser dslParser)
{
return new ReactiveAerospikeTemplate(aerospikeReactorClient, settings.getDataSettings().getNamespace(),
mappingAerospikeConverter, aerospikeMappingContext, aerospikeExceptionTranslator,
- reactorQueryEngine, reactorIndexRefresher, serverVersionSupport);
+ reactorQueryEngine, reactorIndexRefresher, indexCacheHolder, serverVersionSupport, dslParser);
}
@Bean(name = "reactiveAerospikeQueryEngine")
diff --git a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
index 54bf505ca..9f35b3f13 100644
--- a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
+++ b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
@@ -23,6 +23,8 @@
import com.aerospike.client.async.EventPolicy;
import com.aerospike.client.async.NettyEventLoops;
import com.aerospike.client.policy.ClientPolicy;
+import com.aerospike.dsl.DSLParser;
+import com.aerospike.dsl.DSLParserImpl;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
@@ -327,4 +329,9 @@ protected AerospikeSettings aerospikeSettings(AerospikeDataSettings dataSettings
return new AerospikeSettings(connectionSettings, dataSettings);
}
+
+ @Bean
+ public DSLParser dslParser() {
+ return new DSLParserImpl();
+ }
}
diff --git a/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java b/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java
index e779ac702..66127744c 100644
--- a/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java
+++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java
@@ -185,8 +185,8 @@ public interface AerospikeOperations {
* @param document The document to be inserted. Must not be {@literal null}.
* @throws OptimisticLockingFailureException if the document has a version attribute with a different value from
* that found on server.
- * @throws DataAccessException If operation failed (see
- * {@link DefaultAerospikeExceptionTranslator} for details).
+ * @throws DataAccessException If operation failed (see {@link DefaultAerospikeExceptionTranslator}
+ * for details).
*/
void insert(T document);
@@ -200,8 +200,8 @@ public interface AerospikeOperations {
* @param setName Set name to override the set associated with the document.
* @throws OptimisticLockingFailureException if the document has a version attribute with a different value from
* that found on server.
- * @throws DataAccessException If operation failed (see
- * {@link DefaultAerospikeExceptionTranslator} for details).
+ * @throws DataAccessException If operation failed (see {@link DefaultAerospikeExceptionTranslator}
+ * for details).
*/
void insert(T document, String setName);
diff --git a/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java b/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java
index 1c68c5464..477a8f79a 100644
--- a/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java
+++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java
@@ -30,17 +30,22 @@
import com.aerospike.client.query.ResultSet;
import com.aerospike.client.query.Statement;
import com.aerospike.client.task.IndexTask;
+import com.aerospike.dsl.DSLParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.aerospike.convert.AerospikeWriteData;
import org.springframework.data.aerospike.convert.MappingAerospikeConverter;
import org.springframework.data.aerospike.core.model.GroupedEntities;
import org.springframework.data.aerospike.core.model.GroupedKeys;
import org.springframework.data.aerospike.index.IndexesCacheRefresher;
+import org.springframework.data.aerospike.index.IndexesCacheRetriever;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
import org.springframework.data.aerospike.mapping.AerospikePersistentEntity;
import org.springframework.data.aerospike.query.KeyRecordIterator;
import org.springframework.data.aerospike.query.QueryEngine;
import org.springframework.data.aerospike.query.cache.IndexRefresher;
+import org.springframework.data.aerospike.query.cache.IndexesCacheHolder;
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.aerospike.repository.query.Query;
import org.springframework.data.aerospike.server.version.ServerVersionSupport;
@@ -52,7 +57,15 @@
import org.springframework.util.Assert;
import java.time.Instant;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
@@ -83,13 +96,14 @@
*/
@Slf4j
public class AerospikeTemplate extends BaseAerospikeTemplate implements AerospikeOperations,
- IndexesCacheRefresher {
+ IndexesCacheRefresher, IndexesCacheRetriever {
private static final Pattern INDEX_EXISTS_REGEX_PATTERN = Pattern.compile("^FAIL:(-?\\d+).*$");
-
private final IAerospikeClient client;
private final QueryEngine queryEngine;
private final IndexRefresher indexRefresher;
+ private final IndexesCacheHolder indexCacheHolder;
+ private final DSLParser dslParser;
public AerospikeTemplate(IAerospikeClient client,
String namespace,
@@ -98,12 +112,16 @@ public AerospikeTemplate(IAerospikeClient client,
AerospikeExceptionTranslator exceptionTranslator,
QueryEngine queryEngine,
IndexRefresher indexRefresher,
- ServerVersionSupport serverVersionSupport) {
+ IndexesCacheHolder indexCacheHolder,
+ ServerVersionSupport serverVersionSupport,
+ DSLParser dslParser) {
super(namespace, converter, mappingContext, exceptionTranslator, client.copyWritePolicyDefault(),
serverVersionSupport);
this.client = client;
this.queryEngine = queryEngine;
this.indexRefresher = indexRefresher;
+ this.indexCacheHolder = indexCacheHolder;
+ this.dslParser = dslParser;
}
@Override
@@ -121,6 +139,15 @@ public Integer refreshIndexesCache() {
return indexRefresher.refreshIndexes();
}
+ @Override
+ public Map getIndexesCache() {
+ return indexCacheHolder.getAllIndexes();
+ }
+
+ public DSLParser getDSLParser() {
+ return dslParser;
+ }
+
@Override
public void save(T document) {
Assert.notNull(document, "Document must not be null!");
@@ -1055,11 +1082,18 @@ public IntStream findByIdsUsingQueryWithoutMapping(Collection> ids, String set
@Override
public Stream find(Query query, Class entityClass) {
+ Assert.notNull(entityClass, "Entity class name must not be null!");
+ if (query.getCriteriaObject().hasDSLString()) {
+ query.getCriteriaObject().parseDSLString(query.getCriteriaObject().getDSLString(), getDSLParser(),
+ namespace, query.getCriteriaObject().getDSLIndexes());
+ }
return find(query, entityClass, getSetName(entityClass));
}
@Override
public Stream find(Query query, Class entityClass, Class targetClass) {
+ Assert.notNull(entityClass, "Entity class name must not be null!");
+
return find(query, targetClass, getSetName(entityClass));
}
@@ -1102,11 +1136,15 @@ public Stream findAll(Class targetClass, String setName) {
@Override
public Stream findAll(Sort sort, long offset, long limit, Class entityClass) {
+ Assert.notNull(entityClass, "Entity class name must not be null!");
+
return findAll(sort, offset, limit, entityClass, getSetName(entityClass));
}
@Override
public Stream findAll(Sort sort, long offset, long limit, Class entityClass, Class targetClass) {
+ Assert.notNull(entityClass, "Entity class name must not be null!");
+
return findAll(sort, offset, limit, targetClass, getSetName(entityClass));
}
diff --git a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java
index 575cdedb8..064a788d8 100644
--- a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java
+++ b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java
@@ -34,17 +34,22 @@
import com.aerospike.client.query.IndexType;
import com.aerospike.client.query.KeyRecord;
import com.aerospike.client.reactor.IAerospikeReactorClient;
+import com.aerospike.dsl.DSLParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.aerospike.convert.AerospikeWriteData;
import org.springframework.data.aerospike.convert.MappingAerospikeConverter;
import org.springframework.data.aerospike.core.model.GroupedEntities;
import org.springframework.data.aerospike.core.model.GroupedKeys;
import org.springframework.data.aerospike.index.IndexesCacheRefresher;
+import org.springframework.data.aerospike.index.IndexesCacheRetriever;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
import org.springframework.data.aerospike.mapping.AerospikePersistentEntity;
import org.springframework.data.aerospike.mapping.AerospikePersistentProperty;
import org.springframework.data.aerospike.query.ReactorQueryEngine;
+import org.springframework.data.aerospike.query.cache.IndexesCacheHolder;
import org.springframework.data.aerospike.query.cache.ReactorIndexRefresher;
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.aerospike.repository.query.Query;
import org.springframework.data.aerospike.server.version.ServerVersionSupport;
@@ -95,13 +100,15 @@
*/
@Slf4j
public class ReactiveAerospikeTemplate extends BaseAerospikeTemplate implements ReactiveAerospikeOperations,
- IndexesCacheRefresher> {
+ IndexesCacheRefresher>, IndexesCacheRetriever {
private static final Pattern INDEX_EXISTS_REGEX_PATTERN = Pattern.compile("^FAIL:(-?\\d+).*$");
private final IAerospikeReactorClient reactorClient;
private final ReactorQueryEngine reactorQueryEngine;
private final ReactorIndexRefresher reactorIndexRefresher;
+ private final IndexesCacheHolder indexCacheHolder;
+ private final DSLParser dslParser;
public ReactiveAerospikeTemplate(IAerospikeReactorClient reactorClient,
String namespace,
@@ -109,13 +116,21 @@ public ReactiveAerospikeTemplate(IAerospikeReactorClient reactorClient,
AerospikeMappingContext mappingContext,
AerospikeExceptionTranslator exceptionTranslator,
ReactorQueryEngine queryEngine, ReactorIndexRefresher reactorIndexRefresher,
- ServerVersionSupport serverVersionSupport) {
+ IndexesCacheHolder indexCacheHolder,
+ ServerVersionSupport serverVersionSupport,
+ DSLParser dslParser) {
super(namespace, converter, mappingContext, exceptionTranslator,
reactorClient.getAerospikeClient().copyWritePolicyDefault(), serverVersionSupport);
Assert.notNull(reactorClient, "Aerospike reactor client must not be null!");
this.reactorClient = reactorClient;
this.reactorQueryEngine = queryEngine;
this.reactorIndexRefresher = reactorIndexRefresher;
+ this.indexCacheHolder = indexCacheHolder;
+ this.dslParser = dslParser;
+ }
+
+ public DSLParser getDSLParser() {
+ return dslParser;
}
@Override
@@ -123,6 +138,11 @@ public Mono refreshIndexesCache() {
return reactorIndexRefresher.refreshIndexes();
}
+ @Override
+ public Map getIndexesCache() {
+ return indexCacheHolder.getAllIndexes();
+ }
+
@Override
public Mono save(T document) {
Assert.notNull(document, "Document for saving must not be null!");
diff --git a/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java b/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java
new file mode 100644
index 000000000..8db4f77f9
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java
@@ -0,0 +1,11 @@
+package org.springframework.data.aerospike.index;
+
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
+
+import java.util.Map;
+
+public interface IndexesCacheRetriever {
+
+ Map getIndexesCache();
+}
diff --git a/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java b/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java
index 45fac812a..e589ef4a7 100644
--- a/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java
+++ b/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java
@@ -175,7 +175,9 @@ private Record getRecord(Policy policy, Key key, String[] binNames) {
private QueryPolicy getQueryPolicy(Qualifier qualifier, boolean includeBins) {
QueryPolicy queryPolicy = new QueryPolicy(client.getQueryPolicyDefault());
- queryPolicy.filterExp = filterExpressionsBuilder.build(qualifier);
+ queryPolicy.filterExp = qualifier != null && qualifier.hasFilterExpression()
+ ? qualifier.getFilterExpression()
+ : filterExpressionsBuilder.build(qualifier);
queryPolicy.includeBinData = includeBins;
return queryPolicy;
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java b/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java
index af22179e8..bdac29b86 100644
--- a/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java
+++ b/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java
@@ -156,7 +156,9 @@ public Flux selectForCount(String namespace, String set, @Nullable Qu
private QueryPolicy getQueryPolicy(Qualifier qualifier, boolean includeBins) {
QueryPolicy queryPolicy = new QueryPolicy(client.getQueryPolicyDefault());
- queryPolicy.filterExp = filterExpressionsBuilder.build(qualifier);
+ queryPolicy.filterExp = qualifier != null && qualifier.hasFilterExpression()
+ ? qualifier.getFilterExpression()
+ : filterExpressionsBuilder.build(qualifier);
queryPolicy.includeBinData = includeBins;
return queryPolicy;
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java b/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java
index b221f9d45..115918da8 100644
--- a/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java
+++ b/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java
@@ -58,11 +58,18 @@ public Statement build(String namespace, String set, @Nullable Query query, Stri
stmt.setBinNames(binNames);
}
if (queryCriteriaIsNotNull(query)) {
- // logging query
- logQualifierDetails(query.getCriteriaObject(), log);
- // statement's filter is set based either on cardinality (the lowest bin values ratio)
- // or on order (the first processed filter)
- setStatementFilterFromQualifiers(stmt, query.getCriteriaObject());
+ if (!query.getCriteriaObject().hasFilterExpression()) {
+ // logging query
+ logQualifierDetails(query.getCriteriaObject(), log);
+ // statement's filter is set based either on cardinality (the lowest bin values ratio)
+ // or on order (the first processed filter)
+ setStatementFilterFromQualifiers(stmt, query.getCriteriaObject());
+ } else {
+ // logging query
+ logQualifierDetails(query.getCriteriaObject(), log);
+ // statement's filter is set based on parsed DSL expression
+ stmt.setFilter(query.getCriteriaObject().getFilter());
+ }
}
return stmt;
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java
index 257506ac0..3497b645d 100644
--- a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java
+++ b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java
@@ -20,6 +20,7 @@
import org.springframework.data.aerospike.query.model.IndexedField;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
public interface IndexesCache {
@@ -47,4 +48,11 @@ public interface IndexesCache {
* @return True if there is an index for the given indexed field.
*/
boolean hasIndexFor(IndexedField indexedField);
+
+ /**
+ * Get all indexes.
+ *
+ * @return Map of indexes by index keys.
+ */
+ Map getAllIndexes();
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java
index 244f58fb4..3e4a7f8c5 100644
--- a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java
+++ b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java
@@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -61,4 +62,9 @@ public boolean hasIndexFor(IndexedField indexedField) {
public void update(IndexesInfo cache) {
this.cache = cache;
}
+
+ @Override
+ public Map getAllIndexes() {
+ return cache.indexes;
+ }
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java
new file mode 100644
index 000000000..4e4bf823f
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java
@@ -0,0 +1,43 @@
+package org.springframework.data.aerospike.query.qualifier;
+
+import com.aerospike.dsl.Index;
+import org.springframework.data.aerospike.annotation.Beta;
+
+import java.util.Collection;
+
+import static org.springframework.data.aerospike.query.qualifier.QualifierKey.DSL_INDEXES;
+import static org.springframework.data.aerospike.query.qualifier.QualifierKey.DSL_STRING;
+
+@Beta
+public class DSLStringQualifierBuilder extends BaseQualifierBuilder {
+
+ DSLStringQualifierBuilder() {
+ }
+
+ /**
+ * Set DSL String. Mandatory parameter.
+ */
+ public DSLStringQualifierBuilder setDSLString(String dslString) {
+ this.map.put(DSL_STRING, dslString);
+ return this;
+ }
+
+ /**
+ * Set indexes for combined queries to choose from based on cardinality. Optional parameter.
+ */
+ public DSLStringQualifierBuilder setIndexes(Collection indexes) {
+ this.map.put(DSL_INDEXES, indexes);
+ return this;
+ }
+
+ public String getDSLString() {
+ return (String) map.get(DSL_STRING);
+ }
+
+ @Override
+ protected void validate() {
+ if (this.getDSLString() == null) {
+ throw new IllegalArgumentException("Expecting DSL String to be provided");
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java
new file mode 100644
index 000000000..27a02c38c
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java
@@ -0,0 +1,39 @@
+package org.springframework.data.aerospike.query.qualifier;
+
+import com.aerospike.client.exp.Expression;
+import com.aerospike.client.query.Filter;
+import org.springframework.data.aerospike.annotation.Beta;
+
+import static org.springframework.data.aerospike.query.qualifier.QualifierKey.FILTER_EXPRESSION;
+import static org.springframework.data.aerospike.query.qualifier.QualifierKey.SINDEX_FILTER;
+
+@Beta
+public class FilterQualifierBuilder extends BaseQualifierBuilder {
+
+ FilterQualifierBuilder() {
+ }
+
+ /**
+ * Set filter expression. Mandatory parameter.
+ */
+ public FilterQualifierBuilder setExpression(Expression filterExpression) {
+ this.map.put(FILTER_EXPRESSION, filterExpression);
+ return this;
+ }
+
+ public Expression getExpression() {
+ return (Expression) map.get(FILTER_EXPRESSION);
+ }
+
+ /**
+ * Set secondary index filter. Mandatory parameter.
+ */
+ public FilterQualifierBuilder setFilter(Filter filter) {
+ this.map.put(SINDEX_FILTER, filter);
+ return this;
+ }
+
+ public Filter getFilter() {
+ return (Filter) map.get(SINDEX_FILTER);
+ }
+}
diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java
index d40c9d33b..f6a59e940 100644
--- a/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java
+++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java
@@ -70,5 +70,4 @@ private void validateValues() {
"metadataField");
}
}
-
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java
index 7eac569f0..5999e8ad3 100644
--- a/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java
+++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java
@@ -20,7 +20,12 @@
import com.aerospike.client.cdt.CTX;
import com.aerospike.client.command.ParticleType;
import com.aerospike.client.exp.Exp;
+import com.aerospike.client.exp.Expression;
import com.aerospike.client.query.Filter;
+import com.aerospike.dsl.DSLParser;
+import com.aerospike.dsl.Index;
+import com.aerospike.dsl.IndexFilterInput;
+import com.aerospike.dsl.ParsedExpression;
import org.springframework.data.aerospike.annotation.Beta;
import org.springframework.data.aerospike.config.AerospikeDataSettings;
import org.springframework.data.aerospike.query.FilterOperation;
@@ -87,6 +92,16 @@ public static MetadataQualifierBuilder metadataBuilder() {
return new MetadataQualifierBuilder();
}
+ @Beta
+ public static FilterQualifierBuilder filterBuilder() {
+ return new FilterQualifierBuilder();
+ }
+
+ @Beta
+ public static DSLStringQualifierBuilder dslStringBuilder() {
+ return new DSLStringQualifierBuilder();
+ }
+
public FilterOperation getOperation() {
return (FilterOperation) internalMap.get(FILTER_OPERATION);
}
@@ -115,6 +130,13 @@ public void setHasSecIndexFilter(Boolean queryAsFilter) {
internalMap.put(HAS_SINDEX_FILTER, queryAsFilter);
}
+ public void parseDSLString(String dslString, DSLParser dslParser, String namespace, Collection indexes) {
+ ParsedExpression parsedExpr = dslParser.parseExpression(dslString, IndexFilterInput.of(namespace, indexes));
+ Exp exp = parsedExpr.getResultPair().getExp();
+ internalMap.put(FILTER_EXPRESSION, exp == null ? null : Exp.build(exp));
+ internalMap.put(SINDEX_FILTER, parsedExpr.getResultPair().getFilter());
+ }
+
public Boolean hasSecIndexFilter() {
return internalMap.containsKey(HAS_SINDEX_FILTER) && (Boolean) internalMap.get(HAS_SINDEX_FILTER);
}
@@ -175,10 +197,35 @@ public Filter getSecondaryIndexFilter() {
return FilterOperation.valueOf(getOperation().toString()).sIndexFilter(internalMap);
}
+ public boolean hasFilterExpression() {
+ return internalMap.get(FILTER_EXPRESSION) != null;
+ }
+
+ public boolean hasDSLString() {
+ return internalMap.get(DSL_STRING) != null;
+ }
+
+ public Expression getFilterExpression() {
+ return (Expression) internalMap.get(FILTER_EXPRESSION);
+ }
+
public Exp getFilterExp() {
return FilterOperation.valueOf(getOperation().toString()).filterExp(internalMap);
}
+ public Filter getFilter() {
+ return (Filter) internalMap.get(SINDEX_FILTER);
+ }
+
+ public String getDSLString() {
+ return (String) internalMap.get(DSL_STRING);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Collection getDSLIndexes() {
+ return (Collection) internalMap.get(DSL_INDEXES);
+ }
+
protected String luaFieldString(String field) {
return String.format("rec['%s']", field);
}
diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java
index c510b5c28..a1fe65c5b 100644
--- a/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java
+++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java
@@ -15,6 +15,10 @@ public enum QualifierKey {
DOT_PATH,
CTX_ARRAY,
IGNORE_CASE,
+ FILTER_EXPRESSION,
+ SINDEX_FILTER,
+ DSL_STRING,
+ DSL_INDEXES,
KEY,
NESTED_KEY,
VALUE,
diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java
index 116e0eb80..19d07bff6 100644
--- a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java
+++ b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java
@@ -18,6 +18,8 @@
import org.springframework.data.aerospike.core.AerospikeOperations;
import org.springframework.data.aerospike.core.AerospikeTemplate;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
@@ -28,6 +30,7 @@
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -39,30 +42,41 @@
* @author Peter Milne
* @author Jean Mercier
*/
-public class AerospikePartTreeQuery extends BaseAerospikePartTreeQuery {
+public class AerospikePartTreeQuery extends BaseAerospikePartTreeQuery> {
private final AerospikeOperations operations;
+ private final AerospikeQueryMethod queryMethod;
+ private final String namespace;
+ private final Map indexCache;
- public AerospikePartTreeQuery(QueryMethod queryMethod,
+ public AerospikePartTreeQuery(QueryMethod baseQueryMethod,
QueryMethodValueEvaluationContextAccessor evalContextAccessor,
AerospikeTemplate operations,
Class extends AbstractQueryCreator, ?>> queryCreator) {
- super(queryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(),
- operations.getAerospikeConverter(), operations.getServerVersionSupport());
+ super(baseQueryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(),
+ operations.getAerospikeConverter(), operations.getServerVersionSupport(), operations.getDSLParser());
this.operations = operations;
+ this.namespace = operations.getNamespace();
+ this.indexCache = operations.getIndexesCache();
+ // each queryMethod here is AerospikeQueryMethod
+ this.queryMethod = (AerospikeQueryMethod) baseQueryMethod;
}
@Override
@SuppressWarnings({"NullableProblems"})
public Object execute(Object[] parameters) {
ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
- Query query = prepareQuery(parameters, accessor);
Class> targetClass = getTargetClass(accessor);
+ if (queryMethod.hasQueryAnnotation()) {
+ return findByQueryAnnotation(queryMethod, targetClass, namespace, indexCache, parameters);
+ }
+ Query query = prepareQuery(parameters, accessor);
+
// queries with id equality have their own processing flow
if (parameters != null && parameters.length > 0) {
Qualifier criteria = query.getCriteriaObject();
- // only for id EQ, id LIKE queries get SimpleProperty query creator
+ // only for id EQ, id LIKE queries have SimpleProperty query creator
if (criteria.hasSingleId()) {
return runQueryWithIdsEquality(targetClass, getIdValue(criteria), null);
} else {
@@ -150,7 +164,8 @@ private Object processPageQuery(Stream> unprocessedResultsStream, Pageable pag
return new PageImpl<>(resultsPage, pageable, numberOfAllResults);
}
- private Stream> findByQuery(Query query, Class> targetClass) {
+ @Override
+ protected Stream> findByQuery(Query query, Class> targetClass) {
// Run query and map to different target class.
if (targetClass != null && targetClass != entityClass) {
return operations.find(query, entityClass, targetClass);
diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java
new file mode 100644
index 000000000..2c2063f04
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java
@@ -0,0 +1,69 @@
+package org.springframework.data.aerospike.repository.query;
+
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.data.aerospike.annotation.Query;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ConcurrentReferenceHashMap;
+import org.springframework.util.StringUtils;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Optional;
+
+public class AerospikeQueryMethod extends QueryMethod {
+
+ private final Map, Optional> annotationCache;
+ private final Method method;
+
+ /**
+ * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following
+ * invocations of the method given.
+ *
+ * @param method must not be {@literal null}.
+ * @param metadata must not be {@literal null}.
+ * @param factory must not be {@literal null}.
+ */
+ public AerospikeQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
+ super(method, metadata, factory);
+ this.method = method;
+ this.annotationCache = new ConcurrentReferenceHashMap<>();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Optional lookupInAnnotationCache(Class annotationType) {
+ return (Optional ) this.annotationCache.computeIfAbsent(annotationType,
+ it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
+ }
+
+ /**
+ * Returns whether the method has an annotated query.
+ *
+ */
+ public boolean hasQueryAnnotation() {
+ return findQueryAnnotation().isPresent();
+ }
+
+ /**
+ * Returns the DSL expression string declared in the {@link org.springframework.data.aerospike.annotation.Query}
+ * annotation or {@literal null}.
+ *
+ */
+ @Nullable
+ String getQueryAnnotation() {
+ return findQueryAnnotation().orElse(null);
+ }
+
+ private Optional findQueryAnnotation() {
+ return lookupQueryAnnotation() //
+ .map(Query::expression) //
+ .filter(StringUtils::hasText);
+ }
+
+ Optional lookupQueryAnnotation() {
+ return lookupInAnnotationCache(Query.class);
+ }
+}
diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java
index 845e17c46..0001a0366 100644
--- a/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java
+++ b/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java
@@ -15,10 +15,18 @@
*/
package org.springframework.data.aerospike.repository.query;
+import com.aerospike.client.exp.Exp;
+import com.aerospike.client.query.Filter;
+import com.aerospike.dsl.DSLParser;
+import com.aerospike.dsl.IndexFilterInput;
+import com.aerospike.dsl.ParsedExpression;
+import com.aerospike.dsl.exception.AerospikeDSLException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.support.PropertyComparator;
import org.springframework.data.aerospike.convert.MappingAerospikeConverter;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.aerospike.server.version.ServerVersionSupport;
import org.springframework.data.domain.Sort;
@@ -36,8 +44,14 @@
import org.springframework.util.ClassUtils;
import java.lang.reflect.Constructor;
+import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
@@ -45,37 +59,41 @@
* @author Jean Mercier
* @author Igor Ermolenko
*/
-public abstract class BaseAerospikePartTreeQuery implements RepositoryQuery {
+public abstract class BaseAerospikePartTreeQuery implements RepositoryQuery {
- protected final QueryMethod queryMethod;
+ protected final QueryMethod baseQueryMethod;
protected final Class> entityClass;
private final QueryMethodValueEvaluationContextAccessor evaluationContextAccessor;
private final Class extends AbstractQueryCreator, ?>> queryCreator;
private final AerospikeMappingContext context;
private final MappingAerospikeConverter converter;
private final ServerVersionSupport versionSupport;
+ private final DSLParser dslParser;
+ private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("(?> queryCreator,
AerospikeMappingContext context,
- MappingAerospikeConverter converter, ServerVersionSupport versionSupport) {
- this.queryMethod = queryMethod;
+ MappingAerospikeConverter converter, ServerVersionSupport versionSupport,
+ DSLParser dslParser) {
+ this.baseQueryMethod = queryMethod;
this.evaluationContextAccessor = evalContextAccessor;
this.queryCreator = queryCreator;
this.entityClass = queryMethod.getEntityInformation().getJavaType();
this.context = context;
this.converter = converter;
this.versionSupport = versionSupport;
+ this.dslParser = dslParser;
}
@Override
public QueryMethod getQueryMethod() {
- return queryMethod;
+ return baseQueryMethod;
}
protected Query prepareQuery(Object[] parameters, ParametersParameterAccessor accessor) {
- PartTree tree = new PartTree(queryMethod.getName(), entityClass);
+ PartTree tree = new PartTree(baseQueryMethod.getName(), entityClass);
Query baseQuery = createQuery(accessor, tree);
Qualifier criteria = baseQuery.getCriteriaObject();
@@ -103,7 +121,8 @@ protected Query prepareQuery(Object[] parameters, ParametersParameterAccessor ac
if (query.getCriteria() instanceof SpelExpression spelExpression) {
// Create a ValueEvaluationContextProvider using the accessor
- ValueEvaluationContextProvider provider = this.evaluationContextAccessor.create(queryMethod.getParameters());
+ ValueEvaluationContextProvider provider =
+ this.evaluationContextAccessor.create(baseQueryMethod.getParameters());
// Get the ValueEvaluationContext using the provider
ValueEvaluationContext valueContext = provider.getEvaluationContext(parameters);
@@ -124,11 +143,11 @@ Class> getTargetClass(ParametersParameterAccessor accessor) {
return accessor.findDynamicProjection();
}
// DTO projection
- if (!isEntityAssignableFromReturnType(queryMethod)) {
- return queryMethod.getReturnedObjectType();
+ if (!isEntityAssignableFromReturnType(baseQueryMethod)) {
+ return baseQueryMethod.getReturnedObjectType();
}
// No projection - target class will be the entity class.
- return queryMethod.getEntityInformation().getJavaType();
+ return baseQueryMethod.getEntityInformation().getJavaType();
}
public Query createQuery(ParametersParameterAccessor accessor, PartTree tree) {
@@ -182,8 +201,8 @@ protected boolean isDeleteQuery(QueryMethod queryMethod) {
}
/**
- * Find whether entity domain class is assignable from query method's returned object class.
- * Not assignable when using a detached DTO (data transfer object, e.g., for projections).
+ * Find whether entity domain class is assignable from query method's returned object class. Not assignable when
+ * using a detached DTO (data transfer object, e.g., for projections).
*
* @param queryMethod QueryMethod in use
* @return true when entity is assignable from query method's return class, otherwise false
@@ -191,4 +210,81 @@ protected boolean isDeleteQuery(QueryMethod queryMethod) {
protected boolean isEntityAssignableFromReturnType(QueryMethod queryMethod) {
return queryMethod.getEntityInformation().getJavaType().isAssignableFrom(queryMethod.getReturnedObjectType());
}
+
+ protected abstract T findByQuery(Query query, Class> targetClass);
+
+ protected List convertToStringsList(Object[] parameters) {
+ if (parameters == null) {
+ return new ArrayList<>();
+ }
+
+ return IntStream.range(0, parameters.length)
+ .mapToObj(idx -> {
+ Object obj = parameters[idx];
+ if (obj == null) {
+ throw new IllegalArgumentException("Element at index " + idx + " is null");
+ } else if (obj instanceof String str) {
+ // Replace any existing single quotes with escaped quotes
+ str = str.replace("'", "\\'");
+ return "'" + str + "'";
+ } else if (obj instanceof Number || obj instanceof Boolean) {
+ return obj.toString();
+ } else {
+ throw new IllegalArgumentException(String.format(
+ "Element at index %s is expected to be a String, number or boolean, instead got %s",
+ idx, obj.getClass().getName())
+ );
+ }
+ }).collect(Collectors.toList());
+ }
+
+ protected String fillPlaceholders(String input, Object[] parameters) {
+ if (parameters == null || parameters.length == 0) return input;
+
+ List parametersList = convertToStringsList(parameters);
+ // Use regex to find and replace placeholders
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(input);
+ StringBuffer result = new StringBuffer();
+
+ while (matcher.find()) {
+ int index = Integer.parseInt(matcher.group(1));
+ if (index >= 0 && index < parametersList.size()) {
+ // Escape any special characters in the replacement
+ String replacement = Matcher.quoteReplacement(parametersList.get(index));
+ matcher.appendReplacement(result, replacement);
+ } else {
+ throw new AerospikeDSLException("Parameter index out of bounds: " + index);
+ }
+ }
+ matcher.appendTail(result);
+ return result.toString();
+ }
+
+ protected T findByQueryAnnotation(AerospikeQueryMethod queryMethod, Class> targetClass, String namespace,
+ Map indexCache, Object[] parameters) {
+ // Map cached indexes to DSL
+ List indexes = indexCache.values().stream().map(value ->
+ com.aerospike.dsl.Index.builder()
+ .namespace(value.getNamespace())
+ .bin(value.getBin())
+ .indexType(value.getIndexType())
+ .binValuesRatio(value.getBinValuesRatio())
+ .build())
+ .toList();
+ // Parse the given expression
+ ParsedExpression expr = dslParser.parseExpression(
+ fillPlaceholders(queryMethod.getQueryAnnotation(), parameters),
+ IndexFilterInput.of(namespace, indexes)
+ );
+ // Create the query
+ Filter filter = expr.getResultPair().getFilter();
+ Exp exp = expr.getResultPair().getExp();
+ Query query = new Query(
+ Qualifier.filterBuilder()
+ .setFilter(filter)
+ .setExpression(exp == null ? null : Exp.build(exp))
+ .build()
+ );
+ return findByQuery(query, targetClass);
+ }
}
diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java
index d66466afe..514fe5f33 100644
--- a/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java
+++ b/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java
@@ -18,6 +18,8 @@
import org.springframework.data.aerospike.core.ReactiveAerospikeOperations;
import org.springframework.data.aerospike.core.ReactiveAerospikeTemplate;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
+import org.springframework.data.aerospike.query.model.Index;
+import org.springframework.data.aerospike.query.model.IndexKey;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
@@ -31,6 +33,7 @@
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
@@ -40,26 +43,37 @@
/**
* @author Igor Ermolenko
*/
-public class ReactiveAerospikePartTreeQuery extends BaseAerospikePartTreeQuery {
+public class ReactiveAerospikePartTreeQuery extends BaseAerospikePartTreeQuery> {
private final ReactiveAerospikeOperations operations;
+ private final AerospikeQueryMethod queryMethod;
+ private final String namespace;
+ private final Map indexCache;
- public ReactiveAerospikePartTreeQuery(QueryMethod queryMethod,
+ public ReactiveAerospikePartTreeQuery(QueryMethod baseQueryMethod,
QueryMethodValueEvaluationContextAccessor evalContextAccessor,
ReactiveAerospikeTemplate operations,
Class extends AbstractQueryCreator, ?>> queryCreator) {
- super(queryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(),
- operations.getAerospikeConverter(), operations.getServerVersionSupport());
+ super(baseQueryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(),
+ operations.getAerospikeConverter(), operations.getServerVersionSupport(), operations.getDSLParser());
this.operations = operations;
+ this.namespace = operations.getNamespace();
+ this.indexCache = operations.getIndexesCache();
+ // each queryMethod here is AerospikeQueryMethod
+ this.queryMethod = (AerospikeQueryMethod) baseQueryMethod;
}
@Override
@SuppressWarnings({"NullableProblems"})
public Object execute(Object[] parameters) {
- ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
- Query query = prepareQuery(parameters, accessor);
+ ParametersParameterAccessor accessor = new ParametersParameterAccessor(baseQueryMethod.getParameters(), parameters);
Class> targetClass = getTargetClass(accessor);
+ if (queryMethod.hasQueryAnnotation()) {
+ return findByQueryAnnotation(queryMethod, targetClass, namespace, indexCache, parameters);
+ }
+ Query query = prepareQuery(parameters, accessor);
+
// queries with id equality have their own processing flow
if (parameters != null && parameters.length > 0) {
Qualifier criteria = query.getCriteriaObject();
@@ -75,14 +89,14 @@ public Object execute(Object[] parameters) {
}
}
- if (isExistsQuery(queryMethod)) {
- return operations.exists(query, queryMethod.getEntityInformation().getJavaType());
- } else if (isCountQuery(queryMethod)) {
- return operations.count(query, queryMethod.getEntityInformation().getJavaType());
- } else if (isDeleteQuery(queryMethod)) {
- operations.delete(query, queryMethod.getEntityInformation().getJavaType());
+ if (isExistsQuery(baseQueryMethod)) {
+ return operations.exists(query, baseQueryMethod.getEntityInformation().getJavaType());
+ } else if (isCountQuery(baseQueryMethod)) {
+ return operations.count(query, baseQueryMethod.getEntityInformation().getJavaType());
+ } else if (isDeleteQuery(baseQueryMethod)) {
+ operations.delete(query, baseQueryMethod.getEntityInformation().getJavaType());
return Optional.empty();
- } else if (queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
+ } else if (baseQueryMethod.isPageQuery() || baseQueryMethod.isSliceQuery()) {
Pageable pageable = accessor.getPageable();
Flux> unprocessedResults = operations.findUsingQueryWithoutPostProcessing(entityClass, targetClass,
query);
@@ -101,27 +115,27 @@ public Object execute(Object[] parameters) {
}
return getPage(unprocessedResults, size, pageable, query);
});
- } else if (queryMethod.isStreamQuery()) {
+ } else if (baseQueryMethod.isStreamQuery()) {
return findByQuery(query, targetClass).toStream();
- } else if (queryMethod.isCollectionQuery()) {
+ } else if (baseQueryMethod.isCollectionQuery()) {
// Currently there seems to be no way to distinguish return type Collection from Mono etc.,
// so a query method with return type Collection will compile but throw ClassCastException in runtime
return findByQuery(query, targetClass).collectList();
}
- else if (queryMethod.isQueryForEntity() || !isEntityAssignableFromReturnType(queryMethod)) {
+ else if (baseQueryMethod.isQueryForEntity() || !isEntityAssignableFromReturnType(baseQueryMethod)) {
// Queries with Flux and Mono return types including projection queries
return findByQuery(query, targetClass);
}
- throw new UnsupportedOperationException("Query method " + queryMethod.getNamedQueryName() + " is not " +
+ throw new UnsupportedOperationException("Query method " + baseQueryMethod.getNamedQueryName() + " is not " +
"supported");
}
protected Object runQueryWithIdsEquality(Class> targetClass, List ids, Query query) {
- if (isExistsQuery(queryMethod)) {
+ if (isExistsQuery(baseQueryMethod)) {
return operations.existsByIdsUsingQuery(ids, entityClass, query);
- } else if (isCountQuery(queryMethod)) {
+ } else if (isCountQuery(baseQueryMethod)) {
return operations.countByIdsUsingQuery(ids, entityClass, query);
- } else if (isDeleteQuery(queryMethod)) {
+ } else if (isDeleteQuery(baseQueryMethod)) {
return operations.deleteByIdsUsingQuery(ids, entityClass, query);
} else {
return operations.findByIdsUsingQuery(ids, entityClass, targetClass, query);
@@ -129,7 +143,7 @@ protected Object runQueryWithIdsEquality(Class> targetClass, List ids,
}
public Object getPage(List> unprocessedResults, long overallSize, Pageable pageable, Query query) {
- if (queryMethod.isSliceQuery()) {
+ if (baseQueryMethod.isSliceQuery()) {
return processSliceQuery(unprocessedResults, overallSize, pageable, query);
} else {
return processPageQuery(unprocessedResults, overallSize, pageable, query);
@@ -137,7 +151,7 @@ public Object getPage(List> unprocessedResults, long overallSize, Pageable pag
}
public Object getPage(Flux> unprocessedResults, long overallSize, Pageable pageable, Query query) {
- if (queryMethod.isSliceQuery()) {
+ if (baseQueryMethod.isSliceQuery()) {
List> resultsPaginated = applyPostProcessing(unprocessedResults, query).toList();
boolean hasNext = overallSize > pageable.getPageSize() * (pageable.getOffset() + 1);
return new SliceImpl<>(resultsPaginated, pageable, hasNext);
@@ -178,7 +192,8 @@ protected Stream applyPostProcessing(Flux results, Query query) {
return results.toStream();
}
- private Flux> findByQuery(Query query, Class> targetClass) {
+ @Override
+ protected Flux> findByQuery(Query query, Class> targetClass) {
// Run query and map to different target class.
if (targetClass != entityClass) {
return operations.find(query, entityClass, targetClass);
diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java b/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java
index 777bbb023..c41da1732 100644
--- a/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java
+++ b/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.aerospike.repository.support;
+import org.springframework.data.aerospike.repository.query.AerospikeQueryMethod;
import org.springframework.data.aerospike.core.AerospikeTemplate;
import org.springframework.data.aerospike.mapping.AerospikePersistentEntity;
import org.springframework.data.aerospike.mapping.AerospikePersistentProperty;
@@ -31,7 +32,6 @@
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
-import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
@@ -143,7 +143,7 @@ public AerospikeQueryLookupStrategy(QueryMethodValueEvaluationContextAccessor ev
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
ProjectionFactory projectionFactory,
NamedQueries namedQueries) {
- QueryMethod queryMethod = new QueryMethod(method, metadata, projectionFactory);
+ AerospikeQueryMethod queryMethod = new AerospikeQueryMethod(method, metadata, projectionFactory);
return new AerospikePartTreeQuery(queryMethod, evaluationContextAccessor, this.aerospikeTemplate,
this.queryCreator);
}
diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java b/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java
index 92d6090c8..8e6b99008 100644
--- a/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java
+++ b/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java
@@ -19,6 +19,7 @@
import org.springframework.data.aerospike.mapping.AerospikePersistentEntity;
import org.springframework.data.aerospike.mapping.AerospikePersistentProperty;
import org.springframework.data.aerospike.repository.query.AerospikeQueryCreator;
+import org.springframework.data.aerospike.repository.query.AerospikeQueryMethod;
import org.springframework.data.aerospike.repository.query.ReactiveAerospikePartTreeQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
@@ -31,7 +32,6 @@
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
-import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
@@ -144,7 +144,7 @@ public ReactiveAerospikeQueryLookupStrategy(@Nullable Key key,
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
ProjectionFactory projectionFactory,
NamedQueries namedQueries) {
- QueryMethod queryMethod = new QueryMethod(method, metadata, projectionFactory);
+ AerospikeQueryMethod queryMethod = new AerospikeQueryMethod(method, metadata, projectionFactory);
return new ReactiveAerospikePartTreeQuery(queryMethod, evaluationContextAccessor, this.aerospikeTemplate,
this.queryCreator);
}
diff --git a/src/main/java/org/springframework/data/aerospike/util/Utils.java b/src/main/java/org/springframework/data/aerospike/util/Utils.java
index 85213bcc6..7069f0efc 100644
--- a/src/main/java/org/springframework/data/aerospike/util/Utils.java
+++ b/src/main/java/org/springframework/data/aerospike/util/Utils.java
@@ -28,6 +28,7 @@
import org.slf4j.Logger;
import org.springframework.core.env.Environment;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
+import org.springframework.data.aerospike.query.FilterOperation;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
import org.springframework.data.aerospike.repository.query.CriteriaDefinition;
import org.springframework.util.StringUtils;
@@ -176,8 +177,8 @@ public static void logQualifierDetails(CriteriaDefinition criteria, Logger logge
Arrays.stream(qualifiers).forEach(innerQualifier -> logQualifierDetails(innerQualifier, logger));
}
- String operation = qualifier.getOperation().toString();
- operation = (hasLength(operation) ? operation : "N/A");
+ FilterOperation operation = qualifier.getOperation();
+ String strOperation = (operation != null && hasLength(operation.toString()) ? operation.toString() : "N/A");
String values = "";
String value = valueToString(qualifier.getValue());
@@ -202,7 +203,7 @@ public static void logQualifierDetails(CriteriaDefinition criteria, Logger logge
? String.format(", qualifiers = %s,", qualifiersHashesToString(qualifier.getQualifiers()))
: "";
- logger.debug("Created qualifier #{}:{} operation = {}{}{}", qualifier.hashCode(), path, operation, values,
+ logger.debug("Created qualifier #{}:{} operation = {}{}{}", qualifier.hashCode(), path, strOperation, values,
qualifiersStr);
}
diff --git a/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java b/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java
index c948daec8..e75647369 100644
--- a/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java
+++ b/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java
@@ -279,12 +279,12 @@ void stringEndsWithQualifier() {
@Test
void betweenQualifier() {
- // Ages range from 25 -> 29. Get back age between 26 and 28 inclusive
+ // Ages range from 25 -> 29. Get back age between 26 (inclusive) and 28 (inclusive)
Qualifier qualifier = Qualifier.builder()
.setPath("age")
.setFilterOperation(FilterOperation.BETWEEN)
.setValue(26)
- .setSecondValue(29) // + 1 as upper limit is exclusive
+ .setSecondValue(29) // + 1 as upper limit for BETWEEN is exclusive
.build();
KeyRecordIterator it = queryEngine.select(namespace, SET_NAME, null, new Query(qualifier));
diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java
index 1eb9d1361..908b06119 100644
--- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java
+++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java
@@ -1,5 +1,9 @@
package org.springframework.data.aerospike.repository.query.blocking.find;
+import com.aerospike.client.exp.Exp;
+import com.aerospike.client.query.Filter;
+import com.aerospike.client.query.IndexType;
+import com.aerospike.dsl.Index;
import org.junit.jupiter.api.Test;
import org.springframework.data.aerospike.query.FilterOperation;
import org.springframework.data.aerospike.query.qualifier.Qualifier;
@@ -9,6 +13,8 @@
import org.springframework.data.domain.Sort;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -303,5 +309,103 @@ void mapValuesTest() {
.build();
assertThat(repository.findUsingQuery(new Query(intMapWithExactKeyAndValueLt100))).containsOnly(carter);
}
+
+ @Test
+ void findByFilterExpression() {
+ carter.setActive(true);
+ repository.save(carter);
+
+ Qualifier isActiveEqTrue = Qualifier.filterBuilder()
+ .setExpression(
+ Exp.build(
+ Exp.eq(Exp.boolBin("isActive"), Exp.val(true))
+ )
+ ).build();
+ assertThat(repository.findUsingQuery(new Query(isActiveEqTrue))).contains(carter);
+
+ carter.setActive(false);
+ repository.save(carter);
+ }
+
+ @Test
+ void findByDSLString_Exp_only() {
+ carter.setActive(true);
+ repository.save(carter);
+
+ Qualifier isActiveEqTrue = Qualifier.dslStringBuilder()
+ // currently there is no corresponding secondary index Filter for this DSL String, so filter Exp is used
+ .setDSLString("$.isActive.get(type: BOOL) == true")
+ .build();
+ assertThat(repository.findUsingQuery(new Query(isActiveEqTrue))).contains(carter);
+
+ carter.setActive(false);
+ repository.save(carter);
+ }
+
+ @Test
+ void findByDSLString_indexed_Filter() {
+ template.createIndex(Person.class, "firstNameIndex", "firstName", IndexType.STRING);
+
+ List indexes = List.of(
+ Index.builder().namespace("TEST").bin("firstName").indexType(IndexType.STRING).binValuesRatio(0).build()
+ );
+
+ Qualifier firstNameEqCarter = Qualifier.dslStringBuilder()
+ // this DSL String has a corresponding secondary index Filter, so it will be used
+ .setDSLString("$.firstName == 'Carter'")
+ .setIndexes(indexes)
+ .build();
+ assertThat(repository.findUsingQuery(new Query(firstNameEqCarter))).contains(carter);
+
+ Qualifier firstNameEqcarter = Qualifier.dslStringBuilder()
+ // this DSL String has a corresponding secondary index Filter, so it will be used
+ // Person's name "Carter" starts with a capital letter, so no match with lower case first letter
+ .setDSLString("$.firstName == 'carter'")
+ .build();
+ assertThat(repository.findUsingQuery(new Query(firstNameEqcarter))).isEmpty();
+ }
+
+ @Test
+ void findByDSLString_indexed_combined_AND() {
+ template.createIndex(Person.class, "firstNameIndex", "firstName", IndexType.STRING);
+ template.createIndex(Person.class, "lastNameIndex", "lastName", IndexType.STRING);
+
+ // Map indexes cached by Spring Data Aerospike to DSL indexes
+ List indexes = indexesCache.getAllIndexes().values().stream().map(value ->
+ com.aerospike.dsl.Index.builder()
+ .namespace(value.getNamespace())
+ .bin(value.getBin())
+ .indexType(value.getIndexType())
+ .binValuesRatio(value.getBinValuesRatio())
+ .build())
+ .toList();
+
+ Qualifier firstNameAndLastName = Qualifier.dslStringBuilder()
+ // This DSL String has a corresponding secondary index Filter, so it will be used
+ // The index to build the Filter will be chosen from the collection of indexes based by binValuesRatio (cardinality)
+ // If cardinality is the same, the indexed bin to build Filter will be chosen alphabetically
+ .setDSLString("$.firstName == 'Stefan' and $.lastName == 'Lessard'")
+ .setIndexes(indexes)
+ .build();
+ assertThat(firstNameAndLastName.getDSLIndexes().stream()
+ .map(Index::getBinValuesRatio)
+ .collect(Collectors.toSet())).isEqualTo(Set.of(1)); // the same cardinality between two indexes
+ assertThat(repository.findUsingQuery(new Query(firstNameAndLastName))).contains(stefan);
+
+ // This way indexes for DSLParser can be built manually
+ List indexesBuildManually = List.of(
+ Index.builder().namespace("TEST").bin("firstName").indexType(IndexType.STRING).binValuesRatio(0).build(),
+ // Cardinality is different, and it is larger for the "lastName" bin index
+ Index.builder().namespace("TEST").bin("lastName").indexType(IndexType.STRING).binValuesRatio(1).build()
+ );
+ Qualifier firstNameAndLastName2 = Qualifier.dslStringBuilder()
+ // this DSL String has a corresponding secondary index Filter, so it will be used
+ // the index to build the Filter will be chosen from the collection of indexes based by binValuesRatio (cardinality)
+ .setDSLString("$.firstName == 'Stefan' and $.lastName == 'Lessard'")
+ .setIndexes(indexes)
+ .build();
+ // This time "lastName" bin is chosen based on cardinality (binValuesRatio of its index == 1)
+ assertThat(repository.findUsingQuery(new Query(firstNameAndLastName2))).contains(stefan);
+ }
}
diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java
index f95b92051..03274f4d3 100644
--- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java
+++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java
@@ -1,6 +1,6 @@
package org.springframework.data.aerospike.repository.query.blocking.find;
-import com.aerospike.client.Value;
+import com.aerospike.client.query.IndexType;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.aerospike.BaseIntegrationTests;
@@ -72,44 +72,8 @@ void findBySimplePropertyEquals_Enum() {
assertThat(result).containsOnly(alicia);
}
- @Test
- void findBySimplePropertyEquals_BooleanInt() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = false; // save boolean as int
- Person intBoolBinPerson = Person.builder()
- .id(BaseIntegrationTests.nextId())
- .isActive(true)
- .firstName("Test")
- .build();
- repository.save(intBoolBinPerson);
-
- List persons = repository.findByIsActive(true);
- assertThat(persons).contains(intBoolBinPerson);
-
- Value.UseBoolBin = initialValue; // set back to the default value
- repository.delete(intBoolBinPerson);
- }
-
- @Test
- void findBySimplePropertyEquals_BooleanInt_NegativeTest() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = false; // save boolean as int
-
- assertThatThrownBy(() -> negativeTestsRepository.findByIsActive())
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one");
-
- assertThatThrownBy(() -> negativeTestsRepository.findByIsActive(true, false))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one");
-
- Value.UseBoolBin = initialValue; // set back to the default value
- }
-
@Test
void findBySimplePropertyEquals_Boolean() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+
Person intBoolBinPerson = Person.builder().id(BaseIntegrationTests.nextId()).isActive(true).firstName("Test")
.build();
repository.save(intBoolBinPerson);
@@ -117,15 +81,11 @@ void findBySimplePropertyEquals_Boolean() {
List persons = repository.findByIsActive(true);
assertThat(persons).contains(intBoolBinPerson);
- Value.UseBoolBin = initialValue; // set back to the default value
repository.delete(intBoolBinPerson);
}
@Test
void findBySimplePropertyEquals_Boolean_NegativeTest() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+
-
assertThatThrownBy(() -> negativeTestsRepository.findByIsActive())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Person.isActive EQ: invalid number of arguments, expecting one");
@@ -133,8 +93,6 @@ void findBySimplePropertyEquals_Boolean_NegativeTest() {
assertThatThrownBy(() -> negativeTestsRepository.findByIsActive(true, false))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Person.isActive EQ: invalid number of arguments, expecting one");
-
- Value.UseBoolBin = initialValue; // set back to the default value
}
@Test
@@ -216,11 +174,17 @@ void findBySimpleProperty_AND_id_dynamicProjection() {
@Test
void findBySimpleProperty_AND_simpleProperty_DynamicProjection() {
+ additionalAerospikeTestOperations.createIndex(Person.class, "firstNameIdx", "firstName",
+ IndexType.STRING);
+ additionalAerospikeTestOperations.createIndex(Person.class, "lastNameIdx", "lastName",
+ IndexType.STRING);
QueryParam firstName = of(carter.getFirstName());
QueryParam lastName = of(carter.getLastName());
- List result = repository.findByFirstNameAndLastName(firstName, lastName,
- PersonSomeFields.class);
- assertThat(result).containsOnly(carter.toPersonSomeFields());
+// List result = repository.findByFirstNameAndLastName(firstName, lastName,
+// PersonSomeFields.class);
+ List result = repository.findByFirstNameAndLastName(firstName, lastName);
+// assertThat(result).containsOnly(carter.toPersonSomeFields());
+ assertThat(result).containsOnly(carter);
}
@Test
@@ -424,6 +388,13 @@ void findByCollectionEquals() {
}
}
+ @Test
+ void findByCollectionEquals_QueryAnnotation_NegativeTest() {
+ assertThatThrownBy(() -> negativeTestsRepository.findByStringsEquals(List.of("string1", "string2")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Element at index 0 is expected to be a String, number or boolean");
+ }
+
@Test
void findByCollectionEquals_NegativeTest() {
assertThatThrownBy(() -> negativeTestsRepository.findByStrings())
diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java
index 7958657f4..51a4ba044 100644
--- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java
+++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java
@@ -1,6 +1,5 @@
package org.springframework.data.aerospike.repository.query.blocking.find;
-import com.aerospike.client.Value;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.aerospike.repository.query.blocking.PersonRepositoryQueryTests;
@@ -11,31 +10,13 @@
*/
public class FalseTests extends PersonRepositoryQueryTests {
- @Test
- void findByBooleanIntSimplePropertyIsFalse() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = false; // save boolean as int
- Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test")
- .build();
- repository.save(intBoolBinPerson);
-
- Assertions.assertThat(repository.findByIsActiveFalse()).doesNotContain(intBoolBinPerson);
-
- Value.UseBoolBin = initialValue; // set back to the default value
- repository.delete(intBoolBinPerson);
- }
-
@Test
void findByBooleanSimplePropertyIsFalse() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+
Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test")
.build();
repository.save(intBoolBinPerson);
Assertions.assertThat(repository.findByIsActiveFalse()).doesNotContain(intBoolBinPerson);
-
- Value.UseBoolBin = initialValue; // set back to the default value
repository.delete(intBoolBinPerson);
}
}
diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java
index 2f54969a1..58869a3ed 100644
--- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java
+++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java
@@ -1,6 +1,5 @@
package org.springframework.data.aerospike.repository.query.blocking.find;
-import com.aerospike.client.Value;
import org.junit.jupiter.api.Test;
import org.springframework.data.aerospike.repository.query.blocking.PersonRepositoryQueryTests;
import org.springframework.data.aerospike.sample.Person;
@@ -14,28 +13,8 @@
*/
public class TrueTests extends PersonRepositoryQueryTests {
- @Test
- void findByBooleanIntSimplePropertyIsTrue() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = false; // save boolean as int
- Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test")
- .build();
- repository.save(intBoolBinPerson);
-
- List persons1 = repository.findByIsActiveTrue();
- assertThat(persons1).contains(intBoolBinPerson);
-
- List persons2 = repository.findByIsActiveIsTrue(); // another way to call the query method
- assertThat(persons2).containsExactlyElementsOf(persons1);
-
- Value.UseBoolBin = initialValue; // set back to the default value
- repository.delete(intBoolBinPerson);
- }
-
@Test
void findByBooleanSimplePropertyIsTrue() {
- boolean initialValue = Value.UseBoolBin;
- Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+
Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test")
.build();
repository.save(intBoolBinPerson);
@@ -45,8 +24,6 @@ void findByBooleanSimplePropertyIsTrue() {
List persons2 = repository.findByIsActiveIsTrue(); // another way to call the query method
assertThat(persons2).containsExactlyElementsOf(persons1);
-
- Value.UseBoolBin = initialValue; // set back to the default value
repository.delete(intBoolBinPerson);
}
}
diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java
index 307131a97..bf53b2fb3 100644
--- a/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java
+++ b/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java
@@ -84,6 +84,14 @@ public void findBySimpleProperty() {
assertThat(results).containsOnly(homer, marge, bart, lisa, maggie);
}
+ @Test
+ public void findBySimpleProperty_usingQueryAnnotation() {
+ List results = reactiveRepository.findByFirstName("Homer")
+ .collectList().block();
+
+ assertThat(results).containsOnly(homer);
+ }
+
@Test
public void findBySimpleProperty_Projection() {
List results = reactiveRepository.findCustomerSomeFieldsByLastName("Simpson")
diff --git a/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java b/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java
index 9e1046ab1..dc2436aa8 100644
--- a/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java
+++ b/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.aerospike.sample;
+import org.springframework.data.aerospike.annotation.Query;
import org.springframework.data.aerospike.query.QueryParam;
import org.springframework.data.aerospike.repository.AerospikeRepository;
import org.springframework.data.aerospike.repository.query.CriteriaDefinition;
@@ -199,6 +200,11 @@ List findByIntMapContaining(CriteriaDefinition.AerospikeQueryCriterion criter
*/
List
findByStringsEquals(String string1, String string2);
+ /**
+ * Invalid number of arguments: expecting one
+ */
+ @Query(expression = "$.strings == ?0")
+ List
findByStringsEquals(Collection strings);
/**
* Invalid number of arguments: expecting one
*/
diff --git a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java
index 45e786716..d3f9eb8ed 100644
--- a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java
+++ b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java
@@ -16,6 +16,7 @@
package org.springframework.data.aerospike.sample;
import jakarta.validation.constraints.NotNull;
+import org.springframework.data.aerospike.annotation.Query;
import org.springframework.data.aerospike.query.QueryParam;
import org.springframework.data.aerospike.repository.AerospikeRepository;
import org.springframework.data.aerospike.repository.query.CriteriaDefinition.AerospikeNullQueryCriterion;
@@ -67,7 +68,8 @@ public interface PersonRepository extends AerospikeRepository<
List findByLastNameAndId(QueryParam lastName, QueryParam id, Class type);
// Dynamic Projection
- List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName, Class type);
+// List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName, Class type);
+ List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName);
/**
* Find all entities that satisfy the condition "have primary key in the given list and first name equal to the
@@ -377,6 +379,7 @@ List findByAgeOrLastNameLikeAndFirstNameLike(QueryParam age, QueryParam lastN
List
findByAgeIn(ArrayList ages);
+ @Query(expression = "$.isActive.get(type: BOOL) == ?0")
List findByIsActive(boolean isActive);
List
findByIsActiveTrue();
@@ -415,6 +418,7 @@ List
findByAgeOrLastNameLikeAndFirstNameLike(QueryParam age, QueryParam lastN
*
* @param age integer to compare with
*/
+ @Query(expression = "$.age > ?0")
List
findByAgeGreaterThan(int age);
/**
@@ -1594,6 +1598,7 @@ List
findByFriendStringMapNotContaining(AerospikeQueryCriterion criterion,
Page
findTop3ByLastNameStartingWith(String lastName, Pageable pageRequest);
+ @Query(expression = "$.firstName == ?0")
List
findByFirstName(String name);
boolean existsByFirstName(String name);
diff --git a/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java b/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java
index 1f28ab1e9..4b3ebcd96 100644
--- a/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java
+++ b/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.aerospike.sample;
+import org.springframework.data.aerospike.annotation.Query;
import org.springframework.data.aerospike.query.QueryParam;
import org.springframework.data.aerospike.repository.ReactiveAerospikeRepository;
import org.springframework.data.domain.Sort;
@@ -34,6 +35,9 @@ public interface ReactiveCustomerRepository extends ReactiveAerospikeRepository<
Flux findByLastName(String lastName);
+ @Query(expression = "$.firstName == ?0")
+ Flux findByFirstName(String firstName);
+
// DTO Projection
Flux findCustomerSomeFieldsByLastName(String lastName);
diff --git a/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java b/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java
index 9ff84485a..25cca6e4a 100644
--- a/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java
+++ b/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java
@@ -86,7 +86,7 @@ public interface ReactiveIndexedPersonRepository extends ReactiveAerospikeReposi
* field)
*
* @param from lower limit, inclusive
- * @param to upper limit, inclusive
+ * @param to upper limit, exclusive
*/
Flux findByFriendAgeBetween(int from, int to);