diff --git a/client/src/main/java/com/aerospike/ael/Index.java b/client/src/main/java/com/aerospike/ael/Index.java
index 45d7592b..689f1d4c 100644
--- a/client/src/main/java/com/aerospike/ael/Index.java
+++ b/client/src/main/java/com/aerospike/ael/Index.java
@@ -9,10 +9,10 @@
* the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and limitations
+ * under the License.
*/
package com.aerospike.ael;
@@ -30,16 +30,22 @@
* Mandatory fields: {@code namespace}, {@code bin}, {@code indexType}.
* These are validated on build and must not be null/blank (for strings).
* {@code binValuesRatio} defaults to 0 if not set and must not be negative.
+ *
+ * Optional {@code setName} is the Aerospike set this index is defined on; it is used together with
+ * {@link IndexContext} when a query set is supplied for secondary-index selection.
*/
-@Builder
-@EqualsAndHashCode
@Getter
+@EqualsAndHashCode
public class Index {
/**
* Namespace of the indexed bin
*/
private final String namespace;
+ /**
+ * Aerospike set this index is defined on, if known ({@code null} when unspecified).
+ */
+ private final String setName;
/**
* Name of the indexed bin
*/
@@ -66,18 +72,28 @@ public class Index {
*/
private final CTX[] ctx;
- public Index(String namespace, String bin, String name, IndexType indexType, int binValuesRatio,
- IndexCollectionType indexCollectionType, CTX[] ctx) {
- validateMandatory(namespace, bin, indexType, binValuesRatio);
+ @Builder
+ private Index(String namespace, String setName, String bin, String name, IndexType indexType,
+ Integer binValuesRatio, IndexCollectionType indexCollectionType, CTX[] ctx) {
+ int ratio = binValuesRatio != null ? binValuesRatio : 0;
+ validateMandatory(namespace, bin, indexType, ratio);
this.namespace = namespace;
+ this.setName = normalizeSetName(setName);
this.bin = bin;
this.name = name;
this.indexType = indexType;
- this.binValuesRatio = binValuesRatio;
+ this.binValuesRatio = ratio;
this.indexCollectionType = indexCollectionType;
this.ctx = ctx;
}
+ private static String normalizeSetName(String setName) {
+ if (setName == null || setName.isBlank()) {
+ return null;
+ }
+ return setName;
+ }
+
private static void validateMandatory(String namespace, String bin, IndexType indexType, int binValuesRatio) {
requireNonBlank(namespace, "namespace");
requireNonBlank(bin, "bin");
diff --git a/client/src/main/java/com/aerospike/ael/IndexContext.java b/client/src/main/java/com/aerospike/ael/IndexContext.java
index ca163dce..ce46467c 100644
--- a/client/src/main/java/com/aerospike/ael/IndexContext.java
+++ b/client/src/main/java/com/aerospike/ael/IndexContext.java
@@ -9,10 +9,10 @@
* the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and limitations
+ * under the License.
*/
package com.aerospike.ael;
@@ -21,7 +21,11 @@
import java.util.Collection;
/**
- * This class stores namespace and indexes required to build secondary index Filter
+ * This class stores namespace and indexes required to build secondary index Filter.
+ *
+ * When {@link #querySet} is set (same value as the query's set name), only {@link Index} entries whose
+ * {@code setName} matches, or have no set name, are used for secondary-index selection; when it is
+ * {@code null} or blank, no set-based filtering is applied.
*/
@Getter
public class IndexContext {
@@ -43,11 +47,17 @@ public class IndexContext {
* falling back to cardinality-based selection.
*/
private final String preferredBin;
+ /**
+ * Aerospike set name for the query. When non-null, indexes are
+ * filtered by each {@link Index}'s {@code setName} via {@link #indexMatchesQuerySet(Index, String)}.
+ */
+ private final String querySet;
- private IndexContext(String namespace, Collection indexes, String preferredBin) {
+ private IndexContext(String namespace, Collection indexes, String preferredBin, String querySet) {
this.namespace = namespace;
this.indexes = indexes;
this.preferredBin = preferredBin;
+ this.querySet = querySet;
}
/**
@@ -60,7 +70,7 @@ private IndexContext(String namespace, Collection indexes, String preferr
*/
public static IndexContext of(String namespace, Collection indexes) {
validateNamespace(namespace);
- return new IndexContext(namespace, indexes, null);
+ return new IndexContext(namespace, indexes, null, null);
}
/**
@@ -79,14 +89,58 @@ public static IndexContext of(String namespace, Collection indexes) {
public static IndexContext of(String namespace, Collection indexes, String indexToUse) {
validateNamespace(namespace);
if (indexes == null || indexToUse == null || indexToUse.isBlank()) {
- return new IndexContext(namespace, indexes, null);
+ return new IndexContext(namespace, indexes, null, null);
+ }
+ String resolvedBin = indexes.stream()
+ .filter(idx -> indexMatches(idx, namespace, indexToUse))
+ .map(Index::getBin)
+ .findFirst()
+ .orElse(null);
+ return new IndexContext(namespace, indexes, resolvedBin, null);
+ }
+
+ /**
+ * Create index context with namespace, indexes, and query set.
+ * Same as {@link #of(String, Collection)} but applies set-based filtering: only indexes whose
+ * {@link Index} {@code setName} equals {@code querySet}, or have no set name, are used for selection.
+ *
+ * @param namespace Namespace to be used for creating Filter.
+ * Must not be null or blank
+ * @param querySet Aerospike set name for the query; null or blank disables set filtering
+ * @param indexes Collection of {@link Index} objects to be used for creating Filter
+ * @return A new instance of {@code IndexContext}
+ */
+ public static IndexContext withQuerySet(String namespace, String querySet, Collection indexes) {
+ validateNamespace(namespace);
+ return new IndexContext(namespace, indexes, null, normalizeQuerySet(querySet));
+ }
+
+ /**
+ * Create index context with query set and an index-name hint.
+ * The hint is resolved only among indexes that match namespace and {@link #indexMatchesQuerySet(Index, String)}.
+ *
+ * @param namespace Namespace to be used for creating Filter.
+ * Must not be null or blank
+ * @param querySet Aerospike set name for the query; null or blank disables set filtering
+ * @param indexes Collection of {@link Index} objects to be used for creating Filter
+ * @param indexToUse The name of an index to use. If null, blank, or not found among eligible indexes,
+ * index is chosen automatically by cardinality then alphabetically
+ * @return A new instance of {@code IndexContext}
+ */
+ public static IndexContext withQuerySet(String namespace, String querySet, Collection indexes,
+ String indexToUse) {
+ validateNamespace(namespace);
+ String normalized = normalizeQuerySet(querySet);
+ if (indexes == null || indexToUse == null || indexToUse.isBlank()) {
+ return new IndexContext(namespace, indexes, null, normalized);
}
String resolvedBin = indexes.stream()
.filter(idx -> indexMatches(idx, namespace, indexToUse))
+ .filter(idx -> indexMatchesQuerySet(idx, normalized))
.map(Index::getBin)
.findFirst()
.orElse(null);
- return new IndexContext(namespace, indexes, resolvedBin);
+ return new IndexContext(namespace, indexes, resolvedBin, normalized);
}
/**
@@ -106,10 +160,61 @@ public static IndexContext of(String namespace, Collection indexes, Strin
public static IndexContext withBinHint(String namespace, Collection indexes, String binToUse) {
validateNamespace(namespace);
if (indexes == null || binToUse == null || binToUse.isBlank()) {
- return new IndexContext(namespace, indexes, null);
+ return new IndexContext(namespace, indexes, null, null);
}
boolean hasMatch = indexes.stream().anyMatch(idx -> binMatches(idx, namespace, binToUse));
- return new IndexContext(namespace, indexes, hasMatch ? binToUse : null);
+ return new IndexContext(namespace, indexes, hasMatch ? binToUse : null, null);
+ }
+
+ /**
+ * Create index context with a bin name hint and query set.
+ * Same behavior as {@link #withBinHint(String, Collection, String)}, but the bin must exist on an index
+ * that {@link #indexMatchesQuerySet(Index, String)} allows for {@code querySet}.
+ *
+ * @param namespace Namespace to be used for creating Filter.
+ * Must not be null or blank
+ * @param indexes Collection of {@link Index} objects to be used for creating Filter
+ * @param binToUse The name of the bin whose index should be used
+ * @param querySet Aerospike set name for the query; null or blank disables set filtering
+ * @return A new instance of {@code IndexContext}
+ */
+ public static IndexContext withBinHint(String namespace, Collection indexes, String binToUse,
+ String querySet) {
+ validateNamespace(namespace);
+ String normalized = normalizeQuerySet(querySet);
+ if (indexes == null || binToUse == null || binToUse.isBlank()) {
+ return new IndexContext(namespace, indexes, null, normalized);
+ }
+ boolean hasMatch = indexes.stream()
+ .anyMatch(idx -> binMatches(idx, namespace, binToUse) && indexMatchesQuerySet(idx, normalized));
+ return new IndexContext(namespace, indexes, hasMatch ? binToUse : null, normalized);
+ }
+
+ private static String normalizeQuerySet(String querySet) {
+ if (querySet == null || querySet.isBlank()) {
+ return null;
+ }
+ return querySet;
+ }
+
+ /**
+ * Whether {@code idx} may be used when building filters for the given query set.
+ * If {@code querySet} is null, any index is eligible. Otherwise, an index with no set name is eligible;
+ * otherwise its {@link Index} {@code setName} must equal {@code querySet}.
+ *
+ * @param idx secondary index metadata; must not be null
+ * @param querySet normalized query set, or null to disable filtering
+ * @return {@code true} if the index should be included in the catalog passed to filter selection
+ */
+ public static boolean indexMatchesQuerySet(Index idx, String querySet) {
+ if (querySet == null) {
+ return true;
+ }
+ String indexSet = idx.getSetName();
+ if (indexSet == null || indexSet.isBlank()) {
+ return true;
+ }
+ return querySet.equals(indexSet);
}
private static void validateNamespace(String namespace) {
diff --git a/client/src/main/java/com/aerospike/ael/impl/AelParserImpl.java b/client/src/main/java/com/aerospike/ael/impl/AelParserImpl.java
index b6a0026d..f4781113 100644
--- a/client/src/main/java/com/aerospike/ael/impl/AelParserImpl.java
+++ b/client/src/main/java/com/aerospike/ael/impl/AelParserImpl.java
@@ -99,15 +99,20 @@ private ParseTree getParseTree(String input) {
private ParsedExpression getParsedExpression(ParseTree parseTree, PlaceholderValues placeholderValues,
IndexContext indexContext) {
- final String namespace = Optional.ofNullable(indexContext)
- .map(IndexContext::getNamespace)
- .orElse(null);
- Map> indexesMap = buildIndexesMap(
- Optional.ofNullable(indexContext).map(IndexContext::getIndexes).orElse(null), namespace);
- String preferredBin = Optional.ofNullable(indexContext)
- .map(IndexContext::getPreferredBin)
- .orElse(null);
+ String namespace = null;
+ String querySet = null;
+ Collection indexes = null;
+ String preferredBin = null;
+
+ if (indexContext != null) {
+ namespace = indexContext.getNamespace();
+ querySet = indexContext.getQuerySet();
+ indexes = indexContext.getIndexes();
+ preferredBin = indexContext.getPreferredBin();
+ }
+
+ Map> indexesMap = buildIndexesMap(indexes, namespace, querySet);
AbstractPart resultingPart = new ExpressionConditionVisitor().visit(parseTree);
@@ -118,12 +123,14 @@ private ParsedExpression getParsedExpression(ParseTree parseTree, PlaceholderVal
return new ParsedExpression(resultingPart, placeholderValues, indexesMap, preferredBin);
}
- private Map> buildIndexesMap(Collection indexes, String namespace) {
+ private Map> buildIndexesMap(Collection indexes, String namespace,
+ String querySet) {
if (indexes == null || indexes.isEmpty() || namespace == null) {
return Collections.emptyMap();
}
return indexes.stream()
.filter(idx -> namespace.equals(idx.getNamespace()))
+ .filter(idx -> IndexContext.indexMatchesQuerySet(idx, querySet))
.collect(Collectors.groupingBy(Index::getBin));
}
}
diff --git a/client/src/main/java/com/aerospike/client/sdk/AbstractFilterableBuilder.java b/client/src/main/java/com/aerospike/client/sdk/AbstractFilterableBuilder.java
index 3fe763b6..278becc2 100644
--- a/client/src/main/java/com/aerospike/client/sdk/AbstractFilterableBuilder.java
+++ b/client/src/main/java/com/aerospike/client/sdk/AbstractFilterableBuilder.java
@@ -53,10 +53,17 @@ protected void setWhereClause(WhereClauseProcessor clause) {
* Process where clause and return Expression, or null if no clause exists.
*/
protected Expression processWhereClause(String namespace, Session session) {
+ return processWhereClause(namespace, null, session);
+ }
+
+ /**
+ * Process where clause with optional Aerospike set name for secondary-index catalog filtering.
+ */
+ protected Expression processWhereClause(String namespace, String querySet, Session session) {
if (this.ael == null) {
return null;
}
- ParseResult parseResult = this.ael.process(namespace, session);
+ ParseResult parseResult = this.ael.process(namespace, querySet, session);
return Exp.build(parseResult.getExp());
}
diff --git a/client/src/main/java/com/aerospike/client/sdk/BackgroundUdfBuilder.java b/client/src/main/java/com/aerospike/client/sdk/BackgroundUdfBuilder.java
index 972b448c..1db47093 100644
--- a/client/src/main/java/com/aerospike/client/sdk/BackgroundUdfBuilder.java
+++ b/client/src/main/java/com/aerospike/client/sdk/BackgroundUdfBuilder.java
@@ -274,7 +274,7 @@ public ExecuteTask execute() {
Expression filterExp = null;
if (ael != null) {
- ParseResult pr = ael.process(dataset.getNamespace(), session);
+ ParseResult pr = ael.process(dataset.getNamespace(), dataset.getSet(), session);
filter = pr.getFilter();
filterExp = Exp.build(pr.getExp());
}
diff --git a/client/src/main/java/com/aerospike/client/sdk/BinsValuesBuilder.java b/client/src/main/java/com/aerospike/client/sdk/BinsValuesBuilder.java
index cdded80e..85e173f3 100644
--- a/client/src/main/java/com/aerospike/client/sdk/BinsValuesBuilder.java
+++ b/client/src/main/java/com/aerospike/client/sdk/BinsValuesBuilder.java
@@ -818,11 +818,11 @@ private BatchCommand prepareBatch() {
Cluster cluster = session.getCluster();
// Assume all keys have the same namespace.
- String namespace = keys.get(0).namespace;
- Partitions partitions = getPartitions(cluster, namespace);
+ Key firstKey = keys.get(0);
+ Partitions partitions = getPartitions(cluster, firstKey.namespace);
settings = session.getBehavior().getSettings(OpKind.WRITE_RETRYABLE, OpShape.BATCH,
partitions.scMode);
- final Expression filterExp = getFilterExp(session, namespace);
+ final Expression filterExp = getFilterExp(session, firstKey.namespace, firstKey.setName);
BatchAttr attr = new BatchAttr();
attr.setWrite(settings, opBuilder.getOpType());
@@ -838,7 +838,7 @@ private BatchCommand prepareBatch() {
getGeneration(valueSet), ttl));
}
- return new BatchCommand(cluster, partitions, opBuilder.getTxnToUse(), namespace,
+ return new BatchCommand(cluster, partitions, opBuilder.getTxnToUse(), firstKey.namespace,
batchRecords, filterExp, opBuilder.isIncludeMissingKeys(), opBuilder.isFailOnFilteredOut(), false, settings);
}
@@ -875,7 +875,7 @@ private RecordStream executeIndividualSync(ErrorDisposition disposition) {
Partitions partitions = getPartitions(cluster, firstKey.namespace);
ResolvedSettings settings = session.getBehavior().getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
- final Expression filterExp = processWhereClause(keys.get(0).namespace, opBuilder.getSession());
+ final Expression filterExp = processWhereClause(firstKey.namespace, opBuilder.getSession());
if (txnToUse != null) {
TxnMonitor.addKeys(txnToUse, session, keys);
@@ -965,10 +965,10 @@ private RecordStream executeIndividualAsync() {
Cluster cluster = session.getCluster();
// Assume all keys have the same namespace.
- String namespace = keys.get(0).namespace;
- Partitions partitions = getPartitions(cluster, namespace);
+ Key firstKey = keys.get(0);
+ Partitions partitions = getPartitions(cluster, firstKey.namespace);
ResolvedSettings policy = session.getBehavior().getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
- final Expression filterExp = getFilterExp(session, namespace);
+ final Expression filterExp = getFilterExp(session, firstKey.namespace, firstKey.setName);
AsyncRecordStream asyncStream = new AsyncRecordStream(keys.size());
if (txnToUse != null) {
@@ -1054,10 +1054,10 @@ private Partitions getPartitions(Cluster cluster, String namespace) {
return partitions;
}
- private Expression getFilterExp(Session session, String namespace) {
+ private Expression getFilterExp(Session session, String namespace, String querySet) {
if (ael != null) {
// Apply filter expression clause.
- ParseResult parseResult = ael.process(namespace, session);
+ ParseResult parseResult = ael.process(namespace, querySet, session);
return Exp.build(parseResult.getExp());
} else {
return null;
diff --git a/client/src/main/java/com/aerospike/client/sdk/IndexesMonitor.java b/client/src/main/java/com/aerospike/client/sdk/IndexesMonitor.java
index a46f9375..4feb32ee 100644
--- a/client/src/main/java/com/aerospike/client/sdk/IndexesMonitor.java
+++ b/client/src/main/java/com/aerospike/client/sdk/IndexesMonitor.java
@@ -124,6 +124,7 @@ synchronized boolean startMonitor(Session session, Duration frequency) {
session.info().secondaryIndexDetails(sindex, false).ifPresent(details -> {
indexes.add(Index.builder()
.namespace(sindex.getNamespace())
+ .setName(sindex.getSet())
.bin(sindex.getBin())
.indexType(IndexType.valueOf(sindex.getType().name()))
.name(sindex.getIndexName())
diff --git a/client/src/main/java/com/aerospike/client/sdk/ObjectBuilder.java b/client/src/main/java/com/aerospike/client/sdk/ObjectBuilder.java
index dda80c73..57a1aba1 100644
--- a/client/src/main/java/com/aerospike/client/sdk/ObjectBuilder.java
+++ b/client/src/main/java/com/aerospike/client/sdk/ObjectBuilder.java
@@ -1005,7 +1005,7 @@ private BatchCommand prepareBatch() {
records.add(new BatchWrite(key, null, attr, opBuilder.getOpType(), ops, generation, ttl));
}
- final Expression filterExp = getFilterExp(namespace);
+ final Expression filterExp = getFilterExp(namespace, opBuilder.getDataSet().getSet());
return new BatchCommand(cluster, partitions, txnToUse, namespace,
records, filterExp, opBuilder.includeMissingKeys, opBuilder.failOnFilteredOut, false, settings);
@@ -1054,7 +1054,7 @@ private RecordStream executeIndividualSync(ErrorDisposition disposition) {
.getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
// Apply where clause if present
- final Expression filterExp = getFilterExp(firstKey.namespace);
+ final Expression filterExp = getFilterExp(firstKey.namespace, firstKey.setName);
int ttl = (int) resolveTtl(expirationInSeconds, defaultExpirationInSeconds);
if (txnToUse != null) {
@@ -1125,7 +1125,7 @@ private RecordStream executeIndividualAsync() {
.getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
// Apply where clause if present
- final Expression filterExp = getFilterExp(firstKey.namespace);
+ final Expression filterExp = getFilterExp(firstKey.namespace, firstKey.setName);
int ttl = (int) resolveTtl(expirationInSeconds, defaultExpirationInSeconds);
boolean stackTraceOnException = settings.getStackTraceOnException();
@@ -1158,7 +1158,7 @@ private RecordStream executeSingleSync(T element, ErrorDisposition disposition)
.getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
// Apply where clause if present
- final Expression filterExp = getFilterExp(key.namespace);
+ final Expression filterExp = getFilterExp(key.namespace, key.setName);
if (txnToUse != null) {
// Assume all operations are write operations.
@@ -1200,7 +1200,7 @@ private RecordStream executeSingleAsync(T element) {
.getSettings(OpKind.WRITE_RETRYABLE, OpShape.POINT, partitions.scMode);
// Apply where clause if present
- final Expression filterExp = getFilterExp(key.namespace);
+ final Expression filterExp = getFilterExp(key.namespace, key.setName);
int ttl = (int) resolveTtl(expirationInSeconds, defaultExpirationInSeconds);
boolean stackTraceOnException = settings.getStackTraceOnException();
@@ -1285,9 +1285,9 @@ private Partitions getPartitions(Cluster cluster, String namespace) {
return partitions;
}
- private Expression getFilterExp(String namespace) {
+ private Expression getFilterExp(String namespace, String querySet) {
if (opBuilder.getAel() != null && !elements.isEmpty()) {
- ParseResult parseResult = opBuilder.getAel().process(namespace, opBuilder.getSession());
+ ParseResult parseResult = opBuilder.getAel().process(namespace, querySet, opBuilder.getSession());
return Exp.build(parseResult.getExp());
}
return null;
diff --git a/client/src/main/java/com/aerospike/client/sdk/query/WhereClauseProcessor.java b/client/src/main/java/com/aerospike/client/sdk/query/WhereClauseProcessor.java
index 07cb5cb3..da34fe88 100644
--- a/client/src/main/java/com/aerospike/client/sdk/query/WhereClauseProcessor.java
+++ b/client/src/main/java/com/aerospike/client/sdk/query/WhereClauseProcessor.java
@@ -38,8 +38,18 @@
public abstract class WhereClauseProcessor {
protected final boolean allowsIndex;
- public abstract ParseResult process(String namespace, Session session);
+ /**
+ * Parse AEL with no query-set filtering of secondary indexes (legacy behavior).
+ */
+ public final ParseResult process(String namespace, Session session) {
+ return process(namespace, null, session);
+ }
+ /**
+ * Parse AEL; when {@code querySet} is non-null and non-blank, only indexes whose set matches
+ * (or have no set) participate in secondary-index selection.
+ */
+ public abstract ParseResult process(String namespace, String querySet, Session session);
public WhereClauseProcessor(boolean allowsIndex) {
this.allowsIndex = allowsIndex;
@@ -77,7 +87,7 @@ protected String formStringOfFilter(Filter filter, IndexContext indexContext) {
.append(valTypeToString(filter.getValType()))
.append(" ] ")
.append(filterCriteriaToString(filter));
- if (indexContext != null) {
+ if (indexContext != null && indexContext.getIndexes() != null) {
Collection indexes = indexContext.getIndexes();
sb.append("{");
for (Index index : indexes) {
@@ -89,7 +99,7 @@ protected String formStringOfFilter(Filter filter, IndexContext indexContext) {
return sb.toString();
}
- public ParseResult process(String ael, String namespace, Session session) {
+ protected ParseResult process(String ael, String namespace, String querySet, Session session) {
AelParser parser = new AelParserImpl();
ParsedExpression parseResult;
@@ -97,7 +107,7 @@ public ParseResult process(String ael, String namespace, Session session) {
ExpressionContext context = ExpressionContext.of(ael);
if (allowsIndex) {
Set indexes = session.getCluster().getIndexes();
- indexContext = IndexContext.of(namespace, indexes);
+ indexContext = IndexContext.withQuerySet(namespace, querySet, indexes);
parseResult = parser.parseExpression(context, indexContext);
}
else {
@@ -137,8 +147,8 @@ public WhereStringImpl(boolean allowsIndex, String ael) {
}
@Override
- public ParseResult process(String namespace, Session session) {
- return process(this.ael, namespace, session);
+ public ParseResult process(String namespace, String querySet, Session session) {
+ return process(this.ael, namespace, querySet, session);
}
}
@@ -152,10 +162,10 @@ public WherePreparedImpl(boolean allowsIndex, PreparedAel ael, Object... params)
}
@Override
- public ParseResult process(String namespace, Session session) {
+ public ParseResult process(String namespace, String querySet, Session session) {
// TODO: For now, until AEL supports prepared statements
String aelStr = ael.formValue(params);
- return process(aelStr, namespace, session);
+ return process(aelStr, namespace, querySet, session);
}
}
@@ -167,7 +177,8 @@ public WhereBoolExprImpl(boolean allowsIndex, BooleanExpression ael) {
}
@Override
- public ParseResult process(String namespace, Session session) {
+ public ParseResult process(String namespace, String querySet, Session session) {
+ // namespace, querySet, session intentionally ignored - not required in this implementation
return new ParseResult(null, ael.toAerospikeExp());
}
}
@@ -180,7 +191,8 @@ public WhereExpImpl(boolean allowsIndex, Exp exp) {
}
@Override
- public ParseResult process(String namespace, Session session) {
+ public ParseResult process(String namespace, String querySet, Session session) {
+ // namespace, querySet, session intentionally ignored - not required in this implementation
return new ParseResult(null, exp);
}
}
@@ -200,4 +212,4 @@ public static WhereClauseProcessor from(Exp exp) {
public static WhereClauseProcessor from(Expression exp) {
return from(Exp.expr(exp));
}
-}
\ No newline at end of file
+}
diff --git a/client/src/test/java/com/aerospike/ael/index/IndexContextTests.java b/client/src/test/java/com/aerospike/ael/index/IndexContextTests.java
index 2bdb4699..d5a76be8 100644
--- a/client/src/test/java/com/aerospike/ael/index/IndexContextTests.java
+++ b/client/src/test/java/com/aerospike/ael/index/IndexContextTests.java
@@ -51,6 +51,7 @@ void of_accepts_valid_namespace() {
assertThat(ctx.getNamespace()).isEqualTo(NAMESPACE);
assertThat(ctx.getIndexes()).isEmpty();
+ assertThat(ctx.getQuerySet()).isNull();
}
@Test
@@ -215,4 +216,40 @@ void withBinHint_namespace_mismatch_returns_all_indexes() {
assertThat(ctx.getIndexes()).containsExactlyElementsOf(indexes);
}
+
+ @Test
+ void withQuerySet_stores_query_set() {
+ IndexContext ctx = IndexContext.withQuerySet(NAMESPACE, "testScan", List.of(VALID_INDEX));
+
+ assertThat(ctx.getQuerySet()).isEqualTo("testScan");
+ assertThat(ctx.getPreferredBin()).isNull();
+ }
+
+ @Test
+ void withQuerySet_blank_normalized_to_null() {
+ IndexContext ctx = IndexContext.withQuerySet(NAMESPACE, " ", List.of(VALID_INDEX));
+
+ assertThat(ctx.getQuerySet()).isNull();
+ }
+
+ @Test
+ void withQuerySet_4arg_resolves_hint_only_for_matching_set() {
+ Index idxSet = Index.builder().namespace(NAMESPACE).setName("set").bin("age").name("ageidx")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build();
+ Index idxScan = Index.builder().namespace(NAMESPACE).setName("testScan").bin("age").name("age_idx")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build();
+ List indexes = List.of(idxSet, idxScan);
+
+ IndexContext ctx = IndexContext.withQuerySet(NAMESPACE, "testScan", indexes, "age_idx");
+
+ assertThat(ctx.getQuerySet()).isEqualTo("testScan");
+ assertThat(ctx.getPreferredBin()).isEqualTo("age");
+ }
+
+ @Test
+ void indexMatchesQuerySet_mismatched_set_excluded() {
+ Index idx = Index.builder().namespace(NAMESPACE).setName("set").bin("age")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build();
+ assertThat(IndexContext.indexMatchesQuerySet(idx, "testScan")).isFalse();
+ }
}
diff --git a/client/src/test/java/com/aerospike/ael/index/IndexTests.java b/client/src/test/java/com/aerospike/ael/index/IndexTests.java
index a089726b..47c69474 100644
--- a/client/src/test/java/com/aerospike/ael/index/IndexTests.java
+++ b/client/src/test/java/com/aerospike/ael/index/IndexTests.java
@@ -154,5 +154,32 @@ void build_succeeds_with_all_mandatory() {
assertThat(index.getBin()).isEqualTo(BIN);
assertThat(index.getIndexType()).isEqualTo(IndexType.STRING);
assertThat(index.getBinValuesRatio()).isOne();
+ assertThat(index.getSetName()).isNull();
+ }
+
+ @Test
+ void build_accepts_optional_set_name() {
+ Index index = Index.builder()
+ .namespace(NAMESPACE)
+ .setName("mySet")
+ .bin(BIN)
+ .indexType(IndexType.INTEGER)
+ .binValuesRatio(0)
+ .build();
+
+ assertThat(index.getSetName()).isEqualTo("mySet");
+ }
+
+ @Test
+ void build_blank_set_name_normalized_to_null() {
+ Index index = Index.builder()
+ .namespace(NAMESPACE)
+ .setName(" ")
+ .bin(BIN)
+ .indexType(IndexType.INTEGER)
+ .binValuesRatio(0)
+ .build();
+
+ assertThat(index.getSetName()).isNull();
}
}
diff --git a/client/src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java b/client/src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java
new file mode 100644
index 00000000..1bfb80de
--- /dev/null
+++ b/client/src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2012-2026 Aerospike, Inc.
+ *
+ * Portions may be licensed to Aerospike, Inc. under one or more contributor
+ * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.aerospike.ael.parsedExpression;
+
+import com.aerospike.ael.ExpressionContext;
+import com.aerospike.ael.Index;
+import com.aerospike.ael.IndexContext;
+import com.aerospike.client.sdk.cdt.ListReturnType;
+import com.aerospike.client.sdk.exp.Exp;
+import com.aerospike.client.sdk.exp.ListExp;
+import com.aerospike.client.sdk.query.Filter;
+import com.aerospike.client.sdk.query.IndexType;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static com.aerospike.ael.util.TestUtils.NAMESPACE;
+import static com.aerospike.ael.util.TestUtils.parseAelExpressionAndCompare;
+
+/**
+ * Parser-level tests for {@link IndexContext#withQuerySet} via {@link com.aerospike.ael.util.TestUtils#parseAelExpressionAndCompare}
+ * (same pattern as {@link InExprTests}).
+ */
+class QuerySetParsedExpressionTests {
+
+ private static final String TEST_SCAN = "testScan";
+
+ @Test
+ void inAndEq_withQuerySet_eqBinIndexedOnlyOnOtherSet_noSiFilter() {
+ List indexes = List.of(
+ Index.builder().namespace(NAMESPACE).setName("customers").bin("intBin1")
+ .indexType(IndexType.INTEGER).binValuesRatio(0).build(),
+ Index.builder().namespace(NAMESPACE).setName("orders").bin("intBin2")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build()
+ );
+ Filter filter = null;
+ Exp exp = Exp.and(
+ ListExp.getByValue(ListReturnType.EXISTS,
+ Exp.intBin("intBin1"), Exp.val(List.of(1, 2, 3))),
+ Exp.eq(Exp.intBin("intBin2"), Exp.val(100)));
+ parseAelExpressionAndCompare(
+ ExpressionContext.of("$.intBin1 in [1, 2, 3] and $.intBin2 == 100"),
+ filter, exp, IndexContext.withQuerySet(NAMESPACE, "customers", indexes));
+ }
+
+ @Test
+ void inAndEq_withQuerySet_indexesOnQuerySet_sameAsInExpr() {
+ List indexes = List.of(
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin1")
+ .indexType(IndexType.INTEGER).binValuesRatio(0).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin2")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build()
+ );
+ Filter filter = Filter.equal("intBin2", 100);
+ Exp exp = ListExp.getByValue(ListReturnType.EXISTS,
+ Exp.intBin("intBin1"), Exp.val(List.of(1, 2, 3)));
+ parseAelExpressionAndCompare(
+ ExpressionContext.of("$.intBin1 in [1, 2, 3] and $.intBin2 == 100"),
+ filter, exp, IndexContext.withQuerySet(NAMESPACE, TEST_SCAN, indexes));
+ }
+
+ @Test
+ void tripleGt_withoutQuerySet_otherSetWinsCardinality_withQuerySet_usesQuerySetIndexes() {
+ List catalog = List.of(
+ Index.builder().namespace(NAMESPACE).setName("other").bin("intBin1").name("idx_bin1")
+ .indexType(IndexType.INTEGER).binValuesRatio(200).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin2").name("idx_bin2")
+ .indexType(IndexType.INTEGER).binValuesRatio(100).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin3").name("idx_bin3")
+ .indexType(IndexType.INTEGER).binValuesRatio(100).build()
+ );
+ String ael = "$.intBin1 > 50 and $.intBin2 > 50 and $.intBin3 > 50";
+ Exp expWhenSiOnIntBin1 = Exp.and(
+ Exp.gt(Exp.intBin("intBin2"), Exp.val(50)),
+ Exp.gt(Exp.intBin("intBin3"), Exp.val(50)));
+ Exp expWhenSiOnIntBin2 = Exp.and(
+ Exp.gt(Exp.intBin("intBin1"), Exp.val(50)),
+ Exp.gt(Exp.intBin("intBin3"), Exp.val(50)));
+
+ parseAelExpressionAndCompare(
+ ExpressionContext.of(ael),
+ Filter.range("intBin1", 51, Long.MAX_VALUE),
+ expWhenSiOnIntBin1,
+ IndexContext.of(NAMESPACE, catalog));
+
+ parseAelExpressionAndCompare(
+ ExpressionContext.of(ael),
+ Filter.range("intBin2", 51, Long.MAX_VALUE),
+ expWhenSiOnIntBin2,
+ IndexContext.withQuerySet(NAMESPACE, TEST_SCAN, catalog));
+ }
+
+ @Test
+ void inAndGt_withQuerySet_onMatchingSets() {
+ List indexes = List.of(
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin1")
+ .indexType(IndexType.INTEGER).binValuesRatio(1).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin2")
+ .indexType(IndexType.INTEGER).binValuesRatio(0).build()
+ );
+ Filter filter = Filter.range("intBin2", 101, Long.MAX_VALUE);
+ Exp exp = ListExp.getByValue(ListReturnType.EXISTS,
+ Exp.intBin("intBin1"), Exp.val(List.of(10, 20)));
+ parseAelExpressionAndCompare(
+ ExpressionContext.of("$.intBin1 in [10, 20] and $.intBin2 > 100"),
+ filter, exp, IndexContext.withQuerySet(NAMESPACE, TEST_SCAN, indexes));
+ }
+
+ @Test
+ void withQuerySet_andIndexNameHint_selectsBinForNamedIndexOnThatSet() {
+ List catalog = List.of(
+ Index.builder().namespace(NAMESPACE).setName("set").bin("age").name("ageidx")
+ .indexType(IndexType.INTEGER).binValuesRatio(50).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("age").name("age_idx")
+ .indexType(IndexType.INTEGER).binValuesRatio(10).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("score").name("score_idx")
+ .indexType(IndexType.INTEGER).binValuesRatio(5).build()
+ );
+ String ael = "$.age > 18 and $.score > 0 and $.flag == 1";
+ Exp exp = Exp.and(
+ Exp.gt(Exp.intBin("score"), Exp.val(0)),
+ Exp.eq(Exp.intBin("flag"), Exp.val(1)));
+
+ parseAelExpressionAndCompare(
+ ExpressionContext.of(ael),
+ Filter.range("age", 19, Long.MAX_VALUE),
+ exp,
+ IndexContext.withQuerySet(NAMESPACE, TEST_SCAN, catalog, "age_idx"));
+ }
+
+ @Test
+ void withBinHint_withQuerySet_prefersBinWhenPresentOnQuerySet() {
+ List catalog = List.of(
+ Index.builder().namespace(NAMESPACE).setName("other").bin("intBin1")
+ .indexType(IndexType.INTEGER).binValuesRatio(200).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin2")
+ .indexType(IndexType.INTEGER).binValuesRatio(100).build(),
+ Index.builder().namespace(NAMESPACE).setName(TEST_SCAN).bin("intBin3")
+ .indexType(IndexType.INTEGER).binValuesRatio(100).build()
+ );
+ String ael = "$.intBin1 > 50 and $.intBin2 > 50 and $.intBin3 > 50";
+ Exp exp = Exp.and(
+ Exp.gt(Exp.intBin("intBin1"), Exp.val(50)),
+ Exp.gt(Exp.intBin("intBin2"), Exp.val(50)));
+
+ parseAelExpressionAndCompare(
+ ExpressionContext.of(ael),
+ Filter.range("intBin3", 51, Long.MAX_VALUE),
+ exp,
+ IndexContext.withBinHint(NAMESPACE, catalog, "intBin3", TEST_SCAN));
+ }
+}