From 31558f7d4f968721757796e98ee86feef50d28a6 Mon Sep 17 00:00:00 2001 From: Gagan Mishra Date: Tue, 14 Apr 2026 10:44:35 -0700 Subject: [PATCH 1/5] wip: add index for sets --- src/main/java/com/aerospike/ael/Index.java | 27 +++- .../java/com/aerospike/ael/IndexContext.java | 119 ++++++++++++++++-- .../java/com/aerospike/ael/api/AelParser.java | 4 +- .../com/aerospike/ael/impl/AelParserImpl.java | 11 +- .../ael/index/IndexContextTests.java | 93 ++++++++++++++ .../com/aerospike/ael/index/IndexTests.java | 27 ++++ 6 files changed, 265 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/aerospike/ael/Index.java b/src/main/java/com/aerospike/ael/Index.java index b67771d..4eabbd1 100644 --- a/src/main/java/com/aerospike/ael/Index.java +++ b/src/main/java/com/aerospike/ael/Index.java @@ -13,16 +13,21 @@ * 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 with {@link IndexContext#getQuerySet()}. */ -@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 */ @@ -49,18 +54,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/src/main/java/com/aerospike/ael/IndexContext.java b/src/main/java/com/aerospike/ael/IndexContext.java index 0daebe0..c6ab6a8 100644 --- a/src/main/java/com/aerospike/ael/IndexContext.java +++ b/src/main/java/com/aerospike/ael/IndexContext.java @@ -5,7 +5,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 {@code Statement.setSetName}), only {@link Index} entries whose + * {@link Index#getSetName()} 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 { @@ -27,11 +31,17 @@ public class IndexContext { * falling back to cardinality-based selection. */ private final String preferredBin; + /** + * Aerospike set name for the query (same as {@code Statement.setSetName}). When non-null, indexes are + * filtered by {@link Index#getSetName()} 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; } /** @@ -44,7 +54,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); } /** @@ -63,14 +73,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#getSetName()} equals {@code querySet}, or have no set name, are used for selection. + * + * @param namespace Namespace to be used for creating {@link com.aerospike.dsl.client.query.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 {@link com.aerospike.dsl.client.query.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); } /** @@ -90,10 +144,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 {@link com.aerospike.dsl.client.query.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#getSetName()} 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/src/main/java/com/aerospike/ael/api/AelParser.java b/src/main/java/com/aerospike/ael/api/AelParser.java index 8746605..fc633fd 100644 --- a/src/main/java/com/aerospike/ael/api/AelParser.java +++ b/src/main/java/com/aerospike/ael/api/AelParser.java @@ -164,7 +164,9 @@ public interface AelParser { * @param input {@link ExpressionContext} containing input string of dot separated elements. If the input string has * placeholders, matching values must be provided within {@code input} too * @param indexContext Class containing namespace and collection of {@link Index} objects that represent - * existing secondary indexes. Required for creating {@link Filter}. Can be null + * existing secondary indexes (optionally including {@link Index#getSetName()} and + * {@link IndexContext#getQuerySet()} aligned with {@code Statement.setSetName}). + * Required for creating {@link Filter}. Can be null * @return {@link ParsedExpression} object * @throws AelParseException in case of invalid syntax */ diff --git a/src/main/java/com/aerospike/ael/impl/AelParserImpl.java b/src/main/java/com/aerospike/ael/impl/AelParserImpl.java index 3702405..063fc51 100644 --- a/src/main/java/com/aerospike/ael/impl/AelParserImpl.java +++ b/src/main/java/com/aerospike/ael/impl/AelParserImpl.java @@ -86,8 +86,13 @@ private ParsedExpression getParsedExpression(ParseTree parseTree, PlaceholderVal .map(IndexContext::getNamespace) .orElse(null); + String querySet = Optional.ofNullable(indexContext) + .map(IndexContext::getQuerySet) + .orElse(null); Map> indexesMap = buildIndexesMap( - Optional.ofNullable(indexContext).map(IndexContext::getIndexes).orElse(null), namespace); + Optional.ofNullable(indexContext).map(IndexContext::getIndexes).orElse(null), + namespace, + querySet); String preferredBin = Optional.ofNullable(indexContext) .map(IndexContext::getPreferredBin) .orElse(null); @@ -101,10 +106,12 @@ 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/src/test/java/com/aerospike/ael/index/IndexContextTests.java b/src/test/java/com/aerospike/ael/index/IndexContextTests.java index bba905e..71b3fdc 100644 --- a/src/test/java/com/aerospike/ael/index/IndexContextTests.java +++ b/src/test/java/com/aerospike/ael/index/IndexContextTests.java @@ -50,6 +50,7 @@ void of_accepts_valid_namespace() { assertThat(ctx.getNamespace()).isEqualTo(NAMESPACE); assertThat(ctx.getIndexes()).isEmpty(); + assertThat(ctx.getQuerySet()).isNull(); } @Test @@ -214,4 +215,96 @@ void withBinHint_namespace_mismatch_returns_all_indexes() { assertThat(ctx.getIndexes()).containsExactlyElementsOf(indexes); } + + @Test + void of_3arg_resolves_preferred_bin_by_index_name_when_indexes_differ_by_set_name() { + Index idxSet = Index.builder().namespace(NAMESPACE).setName("set").bin("age").name("ageidx") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + Index idxScan = Index.builder().namespace(NAMESPACE).setName("testScan").bin("age").name("age_idx") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + List indexes = List.of(idxSet, idxScan); + + IndexContext ctx = IndexContext.of(NAMESPACE, indexes, "age_idx"); + + assertThat(ctx.getPreferredBin()).isEqualTo("age"); + } + + @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.NUMERIC).binValuesRatio(1).build(); + Index idxScan = Index.builder().namespace(NAMESPACE).setName("testScan").bin("age").name("age_idx") + .indexType(IndexType.NUMERIC).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 withQuerySet_4arg_wrong_set_does_not_resolve_index_name() { + Index idxSet = Index.builder().namespace(NAMESPACE).setName("set").bin("age").name("ageidx") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + List indexes = List.of(idxSet); + + IndexContext ctx = IndexContext.withQuerySet(NAMESPACE, "testScan", indexes, "ageidx"); + + assertThat(ctx.getPreferredBin()).isNull(); + } + + @Test + void withBinHint_4arg_requires_bin_on_matching_set() { + Index idxSet = Index.builder().namespace(NAMESPACE).setName("set").bin("age") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + List indexes = List.of(idxSet); + + IndexContext ctx = IndexContext.withBinHint(NAMESPACE, indexes, "age", "testScan"); + + assertThat(ctx.getPreferredBin()).isNull(); + } + + @Test + void indexMatchesQuerySet_null_query_set_matches_all() { + Index idx = Index.builder().namespace(NAMESPACE).setName("set").bin("age") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + assertThat(IndexContext.indexMatchesQuerySet(idx, null)).isTrue(); + } + + @Test + void indexMatchesQuerySet_blank_index_set_matches_any() { + Index idx = Index.builder().namespace(NAMESPACE).bin("age") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + assertThat(IndexContext.indexMatchesQuerySet(idx, "testScan")).isTrue(); + } + + @Test + void indexMatchesQuerySet_equal_set_matches() { + Index idx = Index.builder().namespace(NAMESPACE).setName("testScan").bin("age") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + assertThat(IndexContext.indexMatchesQuerySet(idx, "testScan")).isTrue(); + } + + @Test + void indexMatchesQuerySet_mismatched_set_excluded() { + Index idx = Index.builder().namespace(NAMESPACE).setName("set").bin("age") + .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); + assertThat(IndexContext.indexMatchesQuerySet(idx, "testScan")).isFalse(); + } } diff --git a/src/test/java/com/aerospike/ael/index/IndexTests.java b/src/test/java/com/aerospike/ael/index/IndexTests.java index 58d0af7..e273d6e 100644 --- a/src/test/java/com/aerospike/ael/index/IndexTests.java +++ b/src/test/java/com/aerospike/ael/index/IndexTests.java @@ -153,5 +153,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.NUMERIC) + .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.NUMERIC) + .binValuesRatio(0) + .build(); + + assertThat(index.getSetName()).isNull(); } } From 3f70aa8d653820f0be2cc76035d6a7c5611bdc29 Mon Sep 17 00:00:00 2001 From: Gagan Mishra Date: Tue, 14 Apr 2026 10:56:57 -0700 Subject: [PATCH 2/5] javadoc fixes --- src/main/java/com/aerospike/ael/Index.java | 3 ++- src/main/java/com/aerospike/ael/IndexContext.java | 8 ++++---- src/main/java/com/aerospike/ael/api/AelParser.java | 4 ++-- .../aerospike/ael/client/fluent/AerospikeComparator.java | 3 +-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/aerospike/ael/Index.java b/src/main/java/com/aerospike/ael/Index.java index 4eabbd1..b695bfc 100644 --- a/src/main/java/com/aerospike/ael/Index.java +++ b/src/main/java/com/aerospike/ael/Index.java @@ -14,7 +14,8 @@ * 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 with {@link IndexContext#getQuerySet()}. + * 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. */ @Getter @EqualsAndHashCode diff --git a/src/main/java/com/aerospike/ael/IndexContext.java b/src/main/java/com/aerospike/ael/IndexContext.java index c6ab6a8..e201702 100644 --- a/src/main/java/com/aerospike/ael/IndexContext.java +++ b/src/main/java/com/aerospike/ael/IndexContext.java @@ -8,7 +8,7 @@ * This class stores namespace and indexes required to build secondary index Filter. *

* When {@link #querySet} is set (same value as {@code Statement.setSetName}), only {@link Index} entries whose - * {@link Index#getSetName()} matches, or have no set name, are used for secondary-index selection; when it is + * {@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 @@ -33,7 +33,7 @@ public class IndexContext { private final String preferredBin; /** * Aerospike set name for the query (same as {@code Statement.setSetName}). When non-null, indexes are - * filtered by {@link Index#getSetName()} via {@link #indexMatchesQuerySet(Index, String)}. + * filtered by each {@link Index}'s {@code setName} via {@link #indexMatchesQuerySet(Index, String)}. */ private final String querySet; @@ -86,7 +86,7 @@ public static IndexContext of(String namespace, Collection indexes, Strin /** * 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#getSetName()} equals {@code querySet}, or have no set name, are used for selection. + * {@link Index} {@code setName} equals {@code querySet}, or have no set name, are used for selection. * * @param namespace Namespace to be used for creating {@link com.aerospike.dsl.client.query.Filter}. * Must not be null or blank @@ -184,7 +184,7 @@ private static String normalizeQuerySet(String 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#getSetName()} must equal {@code querySet}. + * 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 diff --git a/src/main/java/com/aerospike/ael/api/AelParser.java b/src/main/java/com/aerospike/ael/api/AelParser.java index fc633fd..d00a517 100644 --- a/src/main/java/com/aerospike/ael/api/AelParser.java +++ b/src/main/java/com/aerospike/ael/api/AelParser.java @@ -164,8 +164,8 @@ public interface AelParser { * @param input {@link ExpressionContext} containing input string of dot separated elements. If the input string has * placeholders, matching values must be provided within {@code input} too * @param indexContext Class containing namespace and collection of {@link Index} objects that represent - * existing secondary indexes (optionally including {@link Index#getSetName()} and - * {@link IndexContext#getQuerySet()} aligned with {@code Statement.setSetName}). + * existing secondary indexes (optionally including per-index {@code setName} and a query set + * on {@link IndexContext} aligned with {@code Statement.setSetName}). * Required for creating {@link Filter}. Can be null * @return {@link ParsedExpression} object * @throws AelParseException in case of invalid syntax diff --git a/src/main/java/com/aerospike/ael/client/fluent/AerospikeComparator.java b/src/main/java/com/aerospike/ael/client/fluent/AerospikeComparator.java index bdd321a..37d63c3 100644 --- a/src/main/java/com/aerospike/ael/client/fluent/AerospikeComparator.java +++ b/src/main/java/com/aerospike/ael/client/fluent/AerospikeComparator.java @@ -28,8 +28,7 @@ /** * Comparator that orders objects according to the Aerospike server's * type ordering hierarchy: - * NIL(1) < BOOLEAN(2) < INTEGER(3) < STRING(4) < LIST(5) < MAP(6) - * < BYTES(7) < DOUBLE(8) < GEOJSON(9). + * {@code NIL(1) < BOOLEAN(2) < INTEGER(3) < STRING(4) < LIST(5) < MAP(6) < BYTES(7) < DOUBLE(8) < GEOJSON(9)}. *

* Cross-type comparison is strictly by type ordinal — there is no * numeric promotion between INTEGER and DOUBLE. From 69d1b74ec233a4e2c0b883aae7285d45482db56f Mon Sep 17 00:00:00 2001 From: Gagan Mishra Date: Tue, 14 Apr 2026 11:38:38 -0700 Subject: [PATCH 3/5] add tests --- src/test/java/com/aerospike/ael/index/IndexContextTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/aerospike/ael/index/IndexContextTests.java b/src/test/java/com/aerospike/ael/index/IndexContextTests.java index 71b3fdc..d5c2b3e 100644 --- a/src/test/java/com/aerospike/ael/index/IndexContextTests.java +++ b/src/test/java/com/aerospike/ael/index/IndexContextTests.java @@ -217,7 +217,7 @@ void withBinHint_namespace_mismatch_returns_all_indexes() { } @Test - void of_3arg_resolves_preferred_bin_by_index_name_when_indexes_differ_by_set_name() { + void resolves_preferred_bin_by_index_name_when_indexes_differ_by_set_name() { Index idxSet = Index.builder().namespace(NAMESPACE).setName("set").bin("age").name("ageidx") .indexType(IndexType.NUMERIC).binValuesRatio(1).build(); Index idxScan = Index.builder().namespace(NAMESPACE).setName("testScan").bin("age").name("age_idx") From 0bf431851d13accccb18b0f6a8107d95777a7a60 Mon Sep 17 00:00:00 2001 From: Gagan Mishra Date: Tue, 14 Apr 2026 11:40:48 -0700 Subject: [PATCH 4/5] add tests # Conflicts: # src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java --- .../ael/parsedExpression/QuerySetParsedExpressionTests.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java diff --git a/src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java b/src/test/java/com/aerospike/ael/parsedExpression/QuerySetParsedExpressionTests.java new file mode 100644 index 0000000..e69de29 From 0e62a0e15d1b6006cd3d8c2d774417d112171248 Mon Sep 17 00:00:00 2001 From: Gagan Mishra Date: Tue, 28 Apr 2026 09:45:30 -0700 Subject: [PATCH 5/5] apply changes from java sdk --- .../java/com/aerospike/ael/IndexContext.java | 6 ++--- .../com/aerospike/ael/impl/AelParserImpl.java | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/aerospike/ael/IndexContext.java b/src/main/java/com/aerospike/ael/IndexContext.java index e201702..8e4615c 100644 --- a/src/main/java/com/aerospike/ael/IndexContext.java +++ b/src/main/java/com/aerospike/ael/IndexContext.java @@ -88,7 +88,7 @@ public static IndexContext of(String namespace, Collection indexes, Strin * 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 {@link com.aerospike.dsl.client.query.Filter}. + * @param namespace Namespace to be used for creating {@link com.aerospike.ael.client.query.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 @@ -103,7 +103,7 @@ public static IndexContext withQuerySet(String namespace, String querySet, Colle * 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 {@link com.aerospike.dsl.client.query.Filter}. + * @param namespace Namespace to be used for creating {@link com.aerospike.ael.client.query.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 @@ -155,7 +155,7 @@ public static IndexContext withBinHint(String namespace, Collection index * 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 {@link com.aerospike.dsl.client.query.Filter}. + * @param namespace Namespace to be used for creating {@link com.aerospike.ael.client.query.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 diff --git a/src/main/java/com/aerospike/ael/impl/AelParserImpl.java b/src/main/java/com/aerospike/ael/impl/AelParserImpl.java index 063fc51..7acd3a7 100644 --- a/src/main/java/com/aerospike/ael/impl/AelParserImpl.java +++ b/src/main/java/com/aerospike/ael/impl/AelParserImpl.java @@ -82,20 +82,22 @@ 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); + 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(); + } - String querySet = Optional.ofNullable(indexContext) - .map(IndexContext::getQuerySet) - .orElse(null); Map> indexesMap = buildIndexesMap( - Optional.ofNullable(indexContext).map(IndexContext::getIndexes).orElse(null), + indexes, namespace, querySet); - String preferredBin = Optional.ofNullable(indexContext) - .map(IndexContext::getPreferredBin) - .orElse(null); AbstractPart resultingPart = new ExpressionConditionVisitor().visit(parseTree);