Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions client/src/main/java/com/aerospike/ael/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
* <p>
* 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
*/
Expand All @@ -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");
Expand Down
127 changes: 116 additions & 11 deletions client/src/main/java/com/aerospike/ael/IndexContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
* <p>
* 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 {
Expand All @@ -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<Index> indexes, String preferredBin) {
private IndexContext(String namespace, Collection<Index> indexes, String preferredBin, String querySet) {
this.namespace = namespace;
this.indexes = indexes;
this.preferredBin = preferredBin;
this.querySet = querySet;
}

/**
Expand All @@ -60,7 +70,7 @@ private IndexContext(String namespace, Collection<Index> indexes, String preferr
*/
public static IndexContext of(String namespace, Collection<Index> indexes) {
validateNamespace(namespace);
return new IndexContext(namespace, indexes, null);
return new IndexContext(namespace, indexes, null, null);
}

/**
Expand All @@ -79,14 +89,58 @@ public static IndexContext of(String namespace, Collection<Index> indexes) {
public static IndexContext of(String namespace, Collection<Index> 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<Index> 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<Index> 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);
}

/**
Expand All @@ -106,10 +160,61 @@ public static IndexContext of(String namespace, Collection<Index> indexes, Strin
public static IndexContext withBinHint(String namespace, Collection<Index> 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<Index> 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) {
Expand Down
25 changes: 16 additions & 9 deletions client/src/main/java/com/aerospike/ael/impl/AelParserImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, List<Index>> 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<Index> indexes = null;
String preferredBin = null;

if (indexContext != null) {
namespace = indexContext.getNamespace();
querySet = indexContext.getQuerySet();
indexes = indexContext.getIndexes();
preferredBin = indexContext.getPreferredBin();
}

Map<String, List<Index>> indexesMap = buildIndexesMap(indexes, namespace, querySet);

AbstractPart resultingPart = new ExpressionConditionVisitor().visit(parseTree);

Expand All @@ -118,12 +123,14 @@ private ParsedExpression getParsedExpression(ParseTree parseTree, PlaceholderVal
return new ParsedExpression(resultingPart, placeholderValues, indexesMap, preferredBin);
}

private Map<String, List<Index>> buildIndexesMap(Collection<Index> indexes, String namespace) {
private Map<String, List<Index>> buildIndexesMap(Collection<Index> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading
Loading