diff --git a/src/main/java/com/aerospike/dsl/DslParseException.java b/src/main/java/com/aerospike/dsl/DslParseException.java
index 0668285..c7d4d73 100644
--- a/src/main/java/com/aerospike/dsl/DslParseException.java
+++ b/src/main/java/com/aerospike/dsl/DslParseException.java
@@ -10,4 +10,8 @@ public class DslParseException extends RuntimeException {
public DslParseException(String description) {
super(description);
}
+
+ public DslParseException(String description, Throwable cause) {
+ super(description, cause);
+ }
}
diff --git a/src/main/java/com/aerospike/dsl/Index.java b/src/main/java/com/aerospike/dsl/Index.java
index 7c75761..d296c3a 100644
--- a/src/main/java/com/aerospike/dsl/Index.java
+++ b/src/main/java/com/aerospike/dsl/Index.java
@@ -1,5 +1,7 @@
package com.aerospike.dsl;
+import com.aerospike.client.cdt.CTX;
+import com.aerospike.client.query.IndexCollectionType;
import com.aerospike.client.query.IndexType;
import lombok.Builder;
import lombok.EqualsAndHashCode;
@@ -28,7 +30,14 @@ public class Index {
/**
* Cardinality of the index calculated using "sindex-stat" command and looking at the ratio of entries
* to unique bin values for the given secondary index on the node (entries_per_bval)
- *
*/
private int binValuesRatio;
+ /**
+ * {@link IndexCollectionType} of the index
+ */
+ private IndexCollectionType indexCollectionType;
+ /**
+ * Array of {@link CTX} representing context of the index
+ */
+ private CTX[] ctx;
}
diff --git a/src/main/java/com/aerospike/dsl/ParsedExpression.java b/src/main/java/com/aerospike/dsl/ParsedExpression.java
index 136d885..98ef7d6 100644
--- a/src/main/java/com/aerospike/dsl/ParsedExpression.java
+++ b/src/main/java/com/aerospike/dsl/ParsedExpression.java
@@ -61,9 +61,7 @@ public ParseResult getResult(PlaceholderValues placeholderValues) {
AbstractPart resultPart = buildExpr((ExpressionContainer) expressionTree, placeholderValues, indexesMap);
return new ParseResult(resultPart.getFilter(), resultPart.getExp());
} else {
- Filter filter = expressionTree.getFilter();
- Exp exp = expressionTree.getExp();
- return new ParseResult(filter, exp);
+ return new ParseResult(expressionTree.getFilter(), expressionTree.getExp());
}
}
return new ParseResult(null, null);
diff --git a/src/main/java/com/aerospike/dsl/api/DSLParser.java b/src/main/java/com/aerospike/dsl/api/DSLParser.java
index 0a15c86..80d38b2 100644
--- a/src/main/java/com/aerospike/dsl/api/DSLParser.java
+++ b/src/main/java/com/aerospike/dsl/api/DSLParser.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.api;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.client.query.Filter;
import com.aerospike.dsl.DslParseException;
import com.aerospike.dsl.ExpressionContext;
@@ -22,7 +23,7 @@
public interface DSLParser {
/**
- * Parse DSL path into Aerospike filter Expression.
+ * Parse DSL string into {@link ParsedExpression}.
*
* Examples:
*
@@ -88,14 +89,14 @@ public interface DSLParser {
*
*
* @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
+ * placeholders, matching values must be provided within {@code input} too
* @return {@link ParsedExpression} object
* @throws DslParseException in case of invalid syntax
*/
ParsedExpression parseExpression(ExpressionContext input);
/**
- * Parse String DSL path into Aerospike filter Expression.
+ * Parse DSL string into {@link ParsedExpression}.
*
* Examples:
*
@@ -160,12 +161,22 @@ public interface DSLParser {
*
*
*
- * @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 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
* @return {@link ParsedExpression} object
* @throws DslParseException in case of invalid syntax
*/
ParsedExpression parseExpression(ExpressionContext input, IndexContext indexContext);
+
+ /**
+ * Parse DSL path with CDT context into an array of {@link CTX} objects. The argument must represent a path with context,
+ * e.g. $.listBinName.[1], $.mapBinName.ab etc.
+ *
+ * @param dslPath Input string representing path with CDT context, must not be null
+ * @return Array of {@link CTX}
+ * @throws DslParseException in case of invalid syntax
+ */
+ CTX[] parseCTX(String dslPath);
}
diff --git a/src/main/java/com/aerospike/dsl/impl/DSLParserImpl.java b/src/main/java/com/aerospike/dsl/impl/DSLParserImpl.java
index 604fc3f..9f15cf1 100644
--- a/src/main/java/com/aerospike/dsl/impl/DSLParserImpl.java
+++ b/src/main/java/com/aerospike/dsl/impl/DSLParserImpl.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.impl;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.dsl.ConditionLexer;
import com.aerospike.dsl.ConditionParser;
import com.aerospike.dsl.DslParseException;
@@ -23,20 +24,39 @@
import java.util.Optional;
import java.util.stream.Collectors;
+import static com.aerospike.dsl.visitor.VisitorUtils.buildCtx;
+
public class DSLParserImpl implements DSLParser {
+ @Override
@Beta
public ParsedExpression parseExpression(ExpressionContext expressionContext) {
ParseTree parseTree = getParseTree(expressionContext.getExpression());
return getParsedExpression(parseTree, expressionContext.getValues(), null);
}
+ @Override
@Beta
public ParsedExpression parseExpression(ExpressionContext expressionContext, IndexContext indexContext) {
ParseTree parseTree = getParseTree(expressionContext.getExpression());
return getParsedExpression(parseTree, expressionContext.getValues(), indexContext);
}
+ @Override
+ @Beta
+ public CTX[] parseCTX(String pathToCtx) {
+ if (pathToCtx == null || pathToCtx.isBlank()) {
+ throw new DslParseException("Path must not be null or empty");
+ }
+
+ ParseTree parseTree = getParseTree(pathToCtx);
+ try {
+ return buildCtx(new ExpressionConditionVisitor().visit(parseTree));
+ } catch (Exception e) {
+ throw new DslParseException("Could not parse the given DSL path input", e);
+ }
+ }
+
private ParseTree getParseTree(String input) {
ConditionLexer lexer = new ConditionLexer(CharStreams.fromString(input));
ConditionParser parser = new ConditionParser(new CommonTokenStream(lexer));
diff --git a/src/main/java/com/aerospike/dsl/parts/AbstractPart.java b/src/main/java/com/aerospike/dsl/parts/AbstractPart.java
index 5ed389e..d31fcd1 100644
--- a/src/main/java/com/aerospike/dsl/parts/AbstractPart.java
+++ b/src/main/java/com/aerospike/dsl/parts/AbstractPart.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.parts;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.client.exp.Exp;
import com.aerospike.client.query.Filter;
import lombok.Getter;
@@ -13,6 +14,7 @@ public abstract class AbstractPart {
protected PartType partType;
protected Exp exp;
protected Filter filter;
+ protected CTX[] ctx;
protected boolean isPlaceholder;
protected AbstractPart(PartType partType) {
diff --git a/src/main/java/com/aerospike/dsl/parts/path/Path.java b/src/main/java/com/aerospike/dsl/parts/path/Path.java
index 03d9661..72301ff 100644
--- a/src/main/java/com/aerospike/dsl/parts/path/Path.java
+++ b/src/main/java/com/aerospike/dsl/parts/path/Path.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.parts.path;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.client.exp.Exp;
import com.aerospike.dsl.parts.AbstractPart;
import com.aerospike.dsl.parts.cdt.CdtPart;
@@ -48,4 +49,9 @@ public Exp processPath(BasePath basePath, PathFunction pathFunction) {
public Exp getExp() {
return processPath(basePath, pathFunction);
}
+
+ @Override
+ public CTX[] getCtx() {
+ return getContextArray(basePath.getCdtParts(), true);
+ }
}
diff --git a/src/main/java/com/aerospike/dsl/util/PathOperandUtils.java b/src/main/java/com/aerospike/dsl/util/PathOperandUtils.java
index d51985c..b962ae5 100644
--- a/src/main/java/com/aerospike/dsl/util/PathOperandUtils.java
+++ b/src/main/java/com/aerospike/dsl/util/PathOperandUtils.java
@@ -5,14 +5,14 @@
import com.aerospike.client.exp.ListExp;
import com.aerospike.client.exp.MapExp;
import com.aerospike.dsl.parts.AbstractPart;
-import com.aerospike.dsl.parts.path.BasePath;
-import com.aerospike.dsl.parts.path.BinPart;
-import com.aerospike.dsl.parts.path.PathFunction;
import com.aerospike.dsl.parts.cdt.CdtPart;
import com.aerospike.dsl.parts.cdt.list.ListPart;
import com.aerospike.dsl.parts.cdt.list.ListTypeDesignator;
import com.aerospike.dsl.parts.cdt.map.MapPart;
import com.aerospike.dsl.parts.cdt.map.MapTypeDesignator;
+import com.aerospike.dsl.parts.path.BasePath;
+import com.aerospike.dsl.parts.path.BinPart;
+import com.aerospike.dsl.parts.path.PathFunction;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
@@ -21,9 +21,9 @@
import static com.aerospike.dsl.parts.AbstractPart.PartType.LIST_PART;
import static com.aerospike.dsl.parts.AbstractPart.PartType.MAP_PART;
-import static com.aerospike.dsl.parts.path.PathFunction.PathFunctionType.*;
import static com.aerospike.dsl.parts.cdt.list.ListPart.ListPartType.*;
import static com.aerospike.dsl.parts.cdt.map.MapPart.MapPartType.MAP_TYPE_DESIGNATOR;
+import static com.aerospike.dsl.parts.path.PathFunction.PathFunctionType.*;
@UtilityClass
public class PathOperandUtils {
@@ -175,13 +175,11 @@ private static boolean isPrevCdtPartAmbiguous(AbstractPart lastPart) {
* @throws UnsupportedOperationException If the path part type is not supported
*/
public static Exp processGet(BasePath basePath, AbstractPart lastPathPart, Exp.Type valueType, int cdtReturnType) {
- if (lastPathPart.getPartType() == LIST_PART) {
- return doProcessCdtGet(basePath, lastPathPart, valueType, cdtReturnType, (ListPart) lastPathPart);
- } else if (lastPathPart.getPartType() == MAP_PART) {
- return doProcessCdtGet(basePath, lastPathPart, valueType, cdtReturnType, (MapPart) lastPathPart);
+ if (lastPathPart.getPartType() != LIST_PART && lastPathPart.getPartType() != MAP_PART) {
+ throw new UnsupportedOperationException(
+ String.format("Path part type %s is not supported", lastPathPart.getPartType()));
}
- throw new UnsupportedOperationException(
- String.format("Path part type %s is not supported", lastPathPart.getPartType()));
+ return doProcessCdtGet(basePath, lastPathPart, valueType, cdtReturnType);
}
/**
@@ -193,13 +191,12 @@ public static Exp processGet(BasePath basePath, AbstractPart lastPathPart, Exp.T
* @param lastPathPart The last {@link AbstractPart} in the path
* @param valueType The expected {@link Exp.Type} of the value being retrieved
* @param cdtReturnType The CDT return type
- * @param cdtPart The {@link CdtPart} being processed
* @return An {@link Exp} representing the CDT "get" operation
*/
private static Exp doProcessCdtGet(BasePath basePath, AbstractPart lastPathPart, Exp.Type valueType,
- int cdtReturnType, CdtPart cdtPart) {
+ int cdtReturnType) {
// list type designator "[]" can be either after bin name or after path
- if (isListTypeDesignator(cdtPart) || isMapTypeDesignator(cdtPart)) {
+ if (isListTypeDesignator(lastPathPart) || isMapTypeDesignator(lastPathPart)) {
return constructCdtExp(basePath, lastPathPart, valueType, cdtReturnType);
}
@@ -237,7 +234,7 @@ private static boolean isMapTypeDesignator(AbstractPart cdtPart) {
* @param includeLast A boolean indicating whether the last part should be included in the context array
* @return An array of {@link CTX} objects
*/
- private static CTX[] getContextArray(List parts, boolean includeLast) {
+ public static CTX[] getContextArray(List parts, boolean includeLast) {
// Nested (Context) map key access
List context = new ArrayList<>();
diff --git a/src/main/java/com/aerospike/dsl/visitor/VisitorUtils.java b/src/main/java/com/aerospike/dsl/visitor/VisitorUtils.java
index aff4b22..397f069 100644
--- a/src/main/java/com/aerospike/dsl/visitor/VisitorUtils.java
+++ b/src/main/java/com/aerospike/dsl/visitor/VisitorUtils.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.visitor;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.client.exp.Exp;
import com.aerospike.client.query.Filter;
import com.aerospike.client.query.IndexType;
@@ -652,24 +653,53 @@ private static Filter getFilterForDivOrFail(String binName, Pair val
* @param bin The bin part
* @param operand The operand part
* @param type The filter operation type
- * @return The appropriate Filter
+ * @return The appropriate {@link Filter}
* @throws NoApplicableFilterException if no appropriate filter can be created
*/
- private static Filter getFilter(BinPart bin, AbstractPart operand, FilterOperationType type) {
+ private static Filter getFilterFromBin(BinPart bin, AbstractPart operand, FilterOperationType type) {
validateOperands(bin, operand);
- String binName = bin.getBinName();
+ return doGetFilterFromBin(bin, operand, type, null);
+ }
+ /**
+ * Creates a Filter based on a bin and an operand, applies an array of {@link CTX} if provided.
+ *
+ * @param bin The bin part
+ * @param operand The operand part
+ * @param type The filter operation type
+ * @param ctx Array of {@link CTX} objects representing context, can be null
+ * @return The appropriate {@link Filter}
+ * @throws NoApplicableFilterException if no appropriate filter can be created
+ */
+ private static Filter doGetFilterFromBin(BinPart bin, AbstractPart operand, FilterOperationType type,
+ CTX[] ctx) {
+ String binName = bin.getBinName();
return switch (operand.getPartType()) {
case INT_OPERAND -> {
validateComparableTypes(bin.getExpType(), Exp.Type.INT);
- yield getFilterForArithmeticOrFail(binName, ((IntOperand) operand).getValue(), type);
+ yield getFilterForArithmeticOrFail(binName, ((IntOperand) operand).getValue(), type, ctx);
}
- case STRING_OPERAND -> handleStringOperand(bin, binName, ((StringOperand) operand).getValue(), type);
+ case STRING_OPERAND -> handleStringOperand(bin, binName, ((StringOperand) operand).getValue(), type, ctx);
default -> throw new NoApplicableFilterException(
"Operand type not supported: %s".formatted(operand.getPartType()));
};
}
+ /**
+ * Creates a Filter based on a path and an operand.
+ *
+ * @param path The path part
+ * @param operand The operand part
+ * @param type The filter operation type
+ * @return The appropriate {@link Filter}
+ * @throws NoApplicableFilterException if no appropriate filter can be created
+ */
+ private static Filter getFilterFromPath(Path path, AbstractPart operand, FilterOperationType type) {
+ validateOperands(path, operand);
+ BinPart binPart = path.getBasePath().getBinPart();
+ return doGetFilterFromBin(binPart, operand, type, path.getCtx());
+ }
+
/**
* This method is used to generate a {@link Filter} when one of the operands is a {@link BinPart}
* and the other is a {@link StringOperand}. It currently only supports equality (`EQ`) comparisons.
@@ -679,12 +709,13 @@ private static Filter getFilter(BinPart bin, AbstractPart operand, FilterOperati
* @param binName The name of the bin
* @param operandValue The value of {@link StringOperand} involved in the filter
* @param type The type of the filter operation (must be {@link FilterOperationType#EQ})
+ * @param ctx Array of {@link CTX} objects representing CDT context, can be null
* @return An Aerospike {@link Filter} for the string or blob comparison
* @throws NoApplicableFilterException if the filter operation type is not equality
* @throws DslParseException if type validation fails or base64 decoding fails
*/
private static Filter handleStringOperand(BinPart bin, String binName, String operandValue,
- FilterOperationType type) {
+ FilterOperationType type, CTX[] ctx) {
if (type != FilterOperationType.EQ) {
throw new NoApplicableFilterException("Only equality comparison is supported for string operands");
}
@@ -693,43 +724,45 @@ private static Filter handleStringOperand(BinPart bin, String binName, String op
if (bin.getExpType() != null && bin.getExpType().equals(Exp.Type.BLOB)) {
validateComparableTypes(bin.getExpType(), Exp.Type.BLOB);
byte[] value = Base64.getDecoder().decode(operandValue);
- return Filter.equal(binName, value);
+ return Filter.equal(binName, value, ctx);
}
// Handle STRING type
validateComparableTypes(bin.getExpType(), Exp.Type.STRING);
- return Filter.equal(binName, operandValue);
+ return Filter.equal(binName, operandValue, ctx);
}
/**
* Creates a Filter based on two operands and a filter operation type.
*
- * @param left The left operand
- * @param right The right operand
- * @param type The filter operation type
- * @param placeholderValues The {@link PlaceholderValues} to match with placeholders by index
+ * @param left The left operand
+ * @param right The right operand
+ * @param type The filter operation type
* @return The appropriate Filter, or null if no filter can be created
* @throws DslParseException if operands are invalid
*/
- private static Filter getFilterOrNull(AbstractPart left, AbstractPart right, FilterOperationType type,
- PlaceholderValues placeholderValues) {
+ private static Filter getFilterOrNull(AbstractPart left, AbstractPart right, FilterOperationType type) {
validateOperands(left, right);
// Handle bin operands
if (left.getPartType() == BIN_PART) {
- return getFilter((BinPart) left, right, type);
+ return getFilterFromBin((BinPart) left, right, type);
+ } else if (left.getPartType() == PATH_OPERAND) {
+ return getFilterFromPath((Path) left, right, type);
}
if (right.getPartType() == BIN_PART) {
- return getFilter((BinPart) right, left, invertType(type));
+ return getFilterFromBin((BinPart) right, left, invertType(type));
+ } else if (right.getPartType() == PATH_OPERAND) {
+ return getFilterFromPath((Path) right, left, invertType(type));
}
// Handle expressions
if (left.getPartType() == EXPRESSION_CONTAINER) {
- return handleExpressionOperand((ExpressionContainer) left, right, type, placeholderValues);
+ return handleExpressionOperand((ExpressionContainer) left, right, type);
}
if (right.getPartType() == EXPRESSION_CONTAINER) {
- return handleExpressionOperand((ExpressionContainer) right, left, type, placeholderValues);
+ return handleExpressionOperand((ExpressionContainer) right, left, type);
}
return null;
@@ -740,23 +773,22 @@ private static Filter getFilterOrNull(AbstractPart left, AbstractPart right, Fil
* It recursively processes the nested expression to determine if a filter can be generated from it in combination
* with the {@code otherOperand} and the overall {@code type} of the filter operation.
*
- * @param expr The {@link ExpressionContainer} operand
- * @param otherOperand The other operand in the filter condition
- * @param type The type of the filter operation
- * @param placeholderValues The {@link PlaceholderValues} to match with placeholders by index
+ * @param expr The {@link ExpressionContainer} operand
+ * @param otherOperand The other operand in the filter condition
+ * @param type The type of the filter operation
* @return A {@link Filter} if one can be generated from the nested expression, otherwise {@code null}
* @throws DslParseException if operands within the nested expression are null
* @throws NoApplicableFilterException if the nested expression structure is not supported for filtering
*/
private static Filter handleExpressionOperand(ExpressionContainer expr, AbstractPart otherOperand,
- FilterOperationType type, PlaceholderValues placeholderValues) {
+ FilterOperationType type) {
AbstractPart exprLeft = expr.getLeft();
AbstractPart exprRight = expr.getRight();
ExprPartsOperation operation = expr.getOperationType();
validateOperands(exprLeft, exprRight);
- return getFilterFromExpressionOrNull(exprLeft, exprRight, operation, otherOperand, type, placeholderValues);
+ return getFilterFromExpressionOrNull(exprLeft, exprRight, operation, otherOperand, type);
}
/**
@@ -765,19 +797,17 @@ private static Filter handleExpressionOperand(ExpressionContainer expr, Abstract
* by combining it with the {@code externalOperand} and the overall {@code type} of the filter operation.
* It specifically looks for cases where a bin is involved in an arithmetic expression with an external operand.
*
- * @param exprLeft The left part of an expression
- * @param exprRight The right part of an expression
- * @param operationType The operation type of the expression
- * @param externalOperand The operand outside the expression
- * @param type The type of the overall filter operation
- * @param placeholderValues The {@link PlaceholderValues} to match with placeholders by index
+ * @param exprLeft The left part of an expression
+ * @param exprRight The right part of an expression
+ * @param operationType The operation type of the expression
+ * @param externalOperand The operand outside the expression
+ * @param type The type of the overall filter operation
* @return A {@link Filter} if one can be generated, otherwise {@code null}
* @throws NoApplicableFilterException if the expression structure is not supported for filtering
*/
private static Filter getFilterFromExpressionOrNull(AbstractPart exprLeft, AbstractPart exprRight,
ExprPartsOperation operationType,
- AbstractPart externalOperand, FilterOperationType type,
- PlaceholderValues placeholderValues) {
+ AbstractPart externalOperand, FilterOperationType type) {
// Handle bin on left side
if (exprLeft.getPartType() == BIN_PART) {
return handleBinArithmeticExpression((BinPart) exprLeft, exprRight, externalOperand,
@@ -792,7 +822,7 @@ private static Filter getFilterFromExpressionOrNull(AbstractPart exprLeft, Abstr
// Handle nested expressions
if (exprLeft.getPartType() == EXPRESSION_CONTAINER) {
- return getFilterOrNull(exprLeft, exprRight, type, placeholderValues);
+ return getFilterOrNull(exprLeft, exprRight, type);
}
return null;
@@ -899,11 +929,11 @@ private static Filter applyFilterOperator(String binName, IntOperand leftOperand
type = invertType(type);
}
float val = (float) rightValue / leftValue;
- return getFilterForArithmeticOrFail(binName, val, type);
+ return getFilterForArithmeticOrFail(binName, val, type, null);
} else {
throw new UnsupportedOperationException("Not supported");
}
- return getFilterForArithmeticOrFail(binName, value, type);
+ return getFilterForArithmeticOrFail(binName, value, type, null);
}
/**
@@ -912,17 +942,18 @@ private static Filter applyFilterOperator(String binName, IntOperand leftOperand
* @param binName The name of the bin to filter on
* @param value The calculated value from the arithmetic operation
* @param type The type of the filter operation
+ * @param ctx Array of {@link CTX} representing context, can be null
* @return A {@link Filter} representing the condition
* @throws NoApplicableFilterException if the operation type is not supported for secondary index filter
*/
- private static Filter getFilterForArithmeticOrFail(String binName, float value, FilterOperationType type) {
+ private static Filter getFilterForArithmeticOrFail(String binName, float value, FilterOperationType type, CTX[] ctx) {
return switch (type) {
// "$.intBin1 > 100" and "100 < $.intBin1" represent the same Filter
- case GT -> Filter.range(binName, getClosestLongToTheRight(value), Long.MAX_VALUE);
- case GTEQ -> Filter.range(binName, (long) value, Long.MAX_VALUE);
- case LT -> Filter.range(binName, Long.MIN_VALUE, getClosestLongToTheLeft(value));
- case LTEQ -> Filter.range(binName, Long.MIN_VALUE, (long) value);
- case EQ -> Filter.equal(binName, (long) value);
+ case GT -> Filter.range(binName, getClosestLongToTheRight(value), Long.MAX_VALUE, ctx);
+ case GTEQ -> Filter.range(binName, (long) value, Long.MAX_VALUE, ctx);
+ case LT -> Filter.range(binName, Long.MIN_VALUE, getClosestLongToTheLeft(value), ctx);
+ case LTEQ -> Filter.range(binName, Long.MIN_VALUE, (long) value, ctx);
+ case EQ -> Filter.equal(binName, (long) value, ctx);
default ->
throw new NoApplicableFilterException("The operation is not supported by secondary index filter");
};
@@ -996,7 +1027,7 @@ public static AbstractPart buildExpr(ExpressionContainer expr, PlaceholderValues
Filter secondaryIndexFilter = null;
try {
- secondaryIndexFilter = getSIFilter(expr, placeholderValues, indexes);
+ secondaryIndexFilter = getSIFilter(expr, indexes);
} catch (NoApplicableFilterException ignored) {
}
expr.setFilter(secondaryIndexFilter);
@@ -1136,6 +1167,31 @@ private static void replacePlaceholdersInExprContainer(AbstractPart part, Placeh
}
}
+ /**
+ * Builds an array of {@link CTX} for a given path.
+ * This is the main entry point for building context based on the parsed expression tree.
+ *
+ * @param path The {@link AbstractPart} representing the path
+ * @return Array of {@link CTX} objects representing the context, or null
+ * @throws UnsupportedOperationException If the given input expression is not a path, or if it has path function
+ */
+ public static CTX[] buildCtx(AbstractPart path) {
+ if (path.getPartType() == BIN_PART) {
+ // No nested context
+ throw new UnsupportedOperationException("CDT context is not provided");
+ }
+ if (path.getPartType() != PATH_OPERAND) {
+ throw new UnsupportedOperationException(
+ String.format("Unsupported input expression type '%s', please provide only path to convert to CTX[]",
+ path.getPartType())
+ );
+ }
+ if (((Path) path).getPathFunction() != null) {
+ throw new UnsupportedOperationException("Path function is unsupported, please provide only path to convert to CTX[]");
+ }
+ return path.getCtx();
+ }
+
/**
* Returns the {@link Exp} generated for a given {@link ExpressionContainer}.
*
@@ -1355,8 +1411,7 @@ private static Exp getExp(AbstractPart part) {
* @return A secondary index {@link Filter}, or {@code null} if no applicable filter can be generated
* @throws NoApplicableFilterException if the expression operation type is not supported
*/
- private static Filter getSIFilter(ExpressionContainer expr, PlaceholderValues placeholderValues,
- Map> indexes) {
+ private static Filter getSIFilter(ExpressionContainer expr, Map> indexes) {
// If it is an OR query
if (expr.getOperationType() == OR) return null;
@@ -1366,8 +1421,7 @@ private static Filter getSIFilter(ExpressionContainer expr, PlaceholderValues pl
return getFilterOrNull(
chosenExpr.getLeft(),
chosenExpr.getRight(),
- getFilterOperation(chosenExpr.getOperationType()),
- placeholderValues
+ getFilterOperation(chosenExpr.getOperationType())
);
}
@@ -1509,6 +1563,8 @@ private static BinPart getBinPart(ExpressionContainer expr, int depth) {
Consumer binPartRetriever = part -> {
if (part.getPartType() == BIN_PART) {
singleBinPartArray[0] = (BinPart) part;
+ } else if (part.getPartType() == PATH_OPERAND) {
+ singleBinPartArray[0] = ((Path) part).getBasePath().getBinPart();
}
};
Predicate stopOnLogicalExpr = part -> {
diff --git a/src/test/java/com/aerospike/dsl/ctx/CtxTests.java b/src/test/java/com/aerospike/dsl/ctx/CtxTests.java
new file mode 100644
index 0000000..529dd6c
--- /dev/null
+++ b/src/test/java/com/aerospike/dsl/ctx/CtxTests.java
@@ -0,0 +1,182 @@
+package com.aerospike.dsl.ctx;
+
+import com.aerospike.client.Value;
+import com.aerospike.client.cdt.CTX;
+import com.aerospike.dsl.DslParseException;
+import org.junit.jupiter.api.Test;
+
+import static com.aerospike.dsl.util.TestUtils.parseCtx;
+import static com.aerospike.dsl.util.TestUtils.parseCtxAndCompareAsBase64;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CtxTests {
+
+ @Test
+ void listExpression_onlyBin_noCtx() {
+ assertThatThrownBy(() -> parseCtx("$.listBin1"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("CDT context is not provided");
+ }
+
+ @Test
+ void listExpression_emptyOrMalformedInput() {
+ assertThatThrownBy(() -> parseCtx(null))
+ .isInstanceOf(DslParseException.class)
+ .hasMessageContaining("Path must not be null or empty");
+
+ assertThatThrownBy(() -> parseCtx(""))
+ .isInstanceOf(DslParseException.class)
+ .hasMessageContaining("Path must not be null or empty");
+ assertThatThrownBy(() -> parseCtx("$..listBin1"))
+ .isInstanceOf(DslParseException.class)
+ .hasMessageContaining("Could not parse the given DSL path input");
+ assertThatThrownBy(() -> parseCtx("$listBin1"))
+ .isInstanceOf(DslParseException.class)
+ .hasMessageContaining("Could not parse the given DSL path input");
+ }
+
+ @Test
+ void listExpression_oneLevel() {
+ parseCtxAndCompareAsBase64("$.listBin1.[0]",
+ new CTX[]{CTX.listIndex(0)});
+ parseCtxAndCompareAsBase64("$.listBin1.[=100]",
+ new CTX[]{CTX.listValue(Value.get(100))});
+ parseCtxAndCompareAsBase64("$.listBin1.[#-1]",
+ new CTX[]{CTX.listRank(-1)});
+ }
+
+ @Test
+ void listExpression_oneLevel_withPathFunction() {
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[0].get(type: INT)"))
+ .isInstanceOf(DslParseException.class)
+ .hasCauseInstanceOf(UnsupportedOperationException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[=100].get(type: INT, return: VALUE)"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[#-1].asInt()"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ }
+
+ @Test
+ void listExpression_oneLevel_withFullDslExpression() {
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[0] == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[=100].get(type: INT, return: VALUE) == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.listBin1.[#-1].asInt() == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ }
+
+ @Test
+ void listExpression_twoLevels() {
+ parseCtxAndCompareAsBase64("$.listBin1.[0].[1]",
+ new CTX[]{CTX.listIndex(0), CTX.listIndex(1)});
+ parseCtxAndCompareAsBase64("$.listBin1.[0].[=100]",
+ new CTX[]{CTX.listIndex(0), CTX.listValue(Value.get(100))});
+ parseCtxAndCompareAsBase64("$.listBin1.[#-1].[=100]",
+ new CTX[]{CTX.listRank(-1), CTX.listValue(Value.get(100))});
+ }
+
+ @Test
+ void listExpression_threeLevels() {
+ parseCtxAndCompareAsBase64("$.listBin1.[0].[1].[2]",
+ new CTX[]{CTX.listIndex(0), CTX.listIndex(1), CTX.listIndex(2)});
+ parseCtxAndCompareAsBase64("$.listBin1.[#-1].[0].[=100]",
+ new CTX[]{CTX.listRank(-1), CTX.listIndex(0), CTX.listValue(Value.get(100))});
+ parseCtxAndCompareAsBase64("$.listBin1.[#-1].[=100].[0]",
+ new CTX[]{CTX.listRank(-1), CTX.listValue(Value.get(100)), CTX.listIndex(0)});
+ }
+
+ @Test
+ void mapExpression_onlyBin_noCtx() {
+ assertThatThrownBy(() -> parseCtx("$.mapBin1"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("CDT context is not provided");
+ }
+
+ @Test
+ void mapExpression_oneLevel() {
+ parseCtxAndCompareAsBase64("$.mapBin1.a",
+ new CTX[]{CTX.mapKey(Value.get("a"))});
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}",
+ new CTX[]{CTX.mapIndex(0)});
+ parseCtxAndCompareAsBase64("$.mapBin1.{#-1}",
+ new CTX[]{CTX.mapRank(-1)});
+ parseCtxAndCompareAsBase64("$.mapBin1.{=100}",
+ new CTX[]{CTX.mapValue(Value.get(100))});
+ }
+
+ @Test
+ void mapExpression_oneLevel_withPathFunction() {
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.a.get(type: INT)"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{0}.get(type: INT)"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{=100}.get(type: INT, return: VALUE)"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{#-1}.asInt()"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Path function is unsupported, please provide only path to convert to CTX[]");
+ }
+
+ @Test
+ void mapExpression_oneLevel_withFullDslExpression() {
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.a == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{0}.get(type: INT, return: VALUE) == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{=100}.asInt() == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ assertThatThrownBy(() -> parseCtx("$.mapBin1.{#-1}.asInt() == 100"))
+ .isInstanceOf(DslParseException.class)
+ .hasStackTraceContaining("Unsupported input expression type 'EXPRESSION_CONTAINER', " +
+ "please provide only path to convert to CTX[]");
+ }
+
+ @Test
+ void mapExpression_twoLevels() {
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}.a",
+ new CTX[]{CTX.mapIndex(0), CTX.mapKey(Value.get("a"))});
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}.{=100}",
+ new CTX[]{CTX.mapIndex(0), CTX.mapValue(Value.get(100))});
+ parseCtxAndCompareAsBase64("$.mapBin1.{#-1}.{=100}",
+ new CTX[]{CTX.mapRank(-1), CTX.mapValue(Value.get(100))});
+ }
+
+ @Test
+ void mapExpression_threeLevels() {
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}.a.{#-1}",
+ new CTX[]{CTX.mapIndex(0), CTX.mapKey(Value.get("a")), CTX.mapRank(-1)});
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}.{=100}.a",
+ new CTX[]{CTX.mapIndex(0), CTX.mapValue(Value.get(100)), CTX.mapKey(Value.get("a"))});
+ parseCtxAndCompareAsBase64("$.mapBin1.{=100}.{#-1}.{0}",
+ new CTX[]{CTX.mapValue(Value.get(100)), CTX.mapRank(-1), CTX.mapIndex(0)});
+ }
+
+ @Test
+ void combinedListMapExpression_fourLevels() {
+ parseCtxAndCompareAsBase64("$.mapBin1.{0}.a.{#-1}.[=100]",
+ new CTX[]{CTX.mapIndex(0), CTX.mapKey(Value.get("a")), CTX.mapRank(-1), CTX.listValue(Value.get(100))});
+ parseCtxAndCompareAsBase64("$.listBin1.[0].[=100].a.{0}",
+ new CTX[]{CTX.listIndex(0), CTX.listValue(Value.get(100)), CTX.mapKey(Value.get("a")), CTX.mapIndex(0)});
+ parseCtxAndCompareAsBase64("$.mapBin1.{=100}.[#-1].{#-1}.[0]",
+ new CTX[]{CTX.mapValue(Value.get(100)), CTX.listRank(-1), CTX.mapRank(-1), CTX.listIndex(0)});
+ }
+}
diff --git a/src/test/java/com/aerospike/dsl/filter/ListExpressionsTests.java b/src/test/java/com/aerospike/dsl/filter/ListExpressionsTests.java
new file mode 100644
index 0000000..f03ca4f
--- /dev/null
+++ b/src/test/java/com/aerospike/dsl/filter/ListExpressionsTests.java
@@ -0,0 +1,73 @@
+package com.aerospike.dsl.filter;
+
+import com.aerospike.client.Value;
+import com.aerospike.client.cdt.CTX;
+import com.aerospike.client.query.Filter;
+import com.aerospike.client.query.IndexType;
+import com.aerospike.dsl.ExpressionContext;
+import com.aerospike.dsl.Index;
+import com.aerospike.dsl.IndexContext;
+import com.aerospike.dsl.util.TestUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static com.aerospike.client.query.IndexCollectionType.LIST;
+
+class ListExpressionsTests {
+
+ String NAMESPACE = "test1";
+ List INDEXES = List.of(
+ Index.builder().namespace(NAMESPACE).bin("listBin1").indexType(IndexType.NUMERIC).build(),
+ Index.builder().namespace(NAMESPACE).bin("listBin1").indexType(IndexType.STRING)
+ .indexCollectionType(LIST).build(),
+ Index.builder().namespace(NAMESPACE).bin("listBin1").indexType(IndexType.STRING)
+ .indexCollectionType(LIST).ctx(new CTX[]{CTX.listIndex(5)}).build(),
+ Index.builder().namespace(NAMESPACE).bin("listBin1").indexType(IndexType.STRING)
+ .indexCollectionType(LIST).ctx(new CTX[]{CTX.listValue(Value.get(5))}).build()
+ );
+ IndexContext INDEX_FILTER_INPUT = IndexContext.of(NAMESPACE, INDEXES);
+
+ @Test
+ void listExpression() {
+ Filter expected = Filter.equal("listBin1", "stringVal");
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1 == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.get(type: STRING) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(
+ ExpressionContext.of("$.listBin1.get(type: STRING, return: VALUE) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected
+ );
+ }
+
+ @Test
+ void listExpressionNested_oneLevel() {
+ Filter expected = Filter.equal("listBin1", "stringVal", CTX.listIndex(5));
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.[5] == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.[5].get(type: STRING) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(
+ ExpressionContext.of("$.listBin1.[5].get(type: STRING, return: VALUE) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected
+ );
+ }
+
+ @Test
+ void listExpressionNested_twoLevels() {
+ Filter expected = Filter.equal("listBin1", "stringVal", CTX.listIndex(5), CTX.listIndex(1));
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.[5].[1] == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.[5].[1].get(type: STRING) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected);
+ TestUtils.parseFilterAndCompare(
+ ExpressionContext.of("$.listBin1.[5].[1].get(type: STRING, return: VALUE) == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected
+ );
+
+ Filter expected2 = Filter.equal("listBin1", "stringVal", CTX.listValue(Value.get(5)), CTX.listRank(10));
+ TestUtils.parseFilterAndCompare(ExpressionContext.of("$.listBin1.[=5].[#10] == \"stringVal\""),
+ INDEX_FILTER_INPUT, expected2);
+ }
+}
diff --git a/src/test/java/com/aerospike/dsl/util/TestUtils.java b/src/test/java/com/aerospike/dsl/util/TestUtils.java
index f6beec1..351a197 100644
--- a/src/test/java/com/aerospike/dsl/util/TestUtils.java
+++ b/src/test/java/com/aerospike/dsl/util/TestUtils.java
@@ -1,5 +1,6 @@
package com.aerospike.dsl.util;
+import com.aerospike.client.cdt.CTX;
import com.aerospike.client.exp.Exp;
import com.aerospike.client.exp.Expression;
import com.aerospike.client.query.Filter;
@@ -31,7 +32,7 @@ public static Exp parseFilterExp(ExpressionContext expressionContext) {
* Parses the given DSL expression and returns the resulting {@link ParsedExpression} object.
*
* @param expressionContext The {@link ExpressionContext} representing DSL expression
- * @param indexContext The {@link IndexContext} to be used for building secondary index filter
+ * @param indexContext The {@link IndexContext} to be used for building secondary index filter
* @return The {@link Exp} object derived from the parsed filter expression
*/
public static ParsedExpression getParsedExpression(ExpressionContext expressionContext, IndexContext indexContext) {
@@ -44,7 +45,7 @@ public static ParsedExpression getParsedExpression(ExpressionContext expressionC
* {@link Expression}.
*
* @param expressionContext The input representing DSL expression
- * @param expected The expected {@link Exp} object to compare against the parsed result
+ * @param expected The expected {@link Exp} object to compare against the parsed result
*/
public static void parseFilterExpressionAndCompare(ExpressionContext expressionContext, Exp expected) {
Expression actualExpression = Exp.build(parser.parseExpression(expressionContext).getResult().getExp());
@@ -67,7 +68,7 @@ public static Filter parseFilter(ExpressionContext expressionContext) {
* Parses the given DL expression using the provided {@link IndexContext} and returns the resulting {@link Filter} object.
*
* @param expressionContext The input representing DSL expression
- * @param indexContext The {@link IndexContext} to be used for building secondary index filter
+ * @param indexContext The {@link IndexContext} to be used for building secondary index filter
* @return A {@link Filter} object derived from the parsed result
*/
public static Filter parseFilter(ExpressionContext expressionContext, IndexContext indexContext) {
@@ -104,8 +105,8 @@ public static void parseFilterAndCompare(ExpressionContext input, IndexContext i
* {@link Filter} and {@link Exp} components with the expected {@code filter} and {@code exp}.
*
* @param expressionContext The input representing DSL expression
- * @param filter The expected {@link Filter} component of the parsed result
- * @param exp The expected {@link Exp} component of the parsed result. Can be {@code null}
+ * @param filter The expected {@link Filter} component of the parsed result
+ * @param exp The expected {@link Exp} component of the parsed result. Can be {@code null}
*/
public static void parseDslExpressionAndCompare(ExpressionContext expressionContext, Filter filter, Exp exp) {
ParsedExpression actualExpression = parser.parseExpression(expressionContext);
@@ -119,9 +120,9 @@ public static void parseDslExpressionAndCompare(ExpressionContext expressionCont
* and compares the resulting {@link Filter} and {@link Exp} components with the expected {@code filter} and {@code exp}.
*
* @param expressionContext The input representing DSL expression
- * @param filter The expected {@link Filter} component of the parsed result
- * @param exp The expected {@link Exp} component of the parsed result. Can be {@code null}
- * @param indexContext The {@link IndexContext} to be used for building secondary index filter
+ * @param filter The expected {@link Filter} component of the parsed result
+ * @param exp The expected {@link Exp} component of the parsed result. Can be {@code null}
+ * @param indexContext The {@link IndexContext} to be used for building secondary index filter
*/
public static void parseDslExpressionAndCompare(ExpressionContext expressionContext, Filter filter, Exp exp, IndexContext indexContext) {
ParsedExpression actualExpression = parser.parseExpression(expressionContext, indexContext);
@@ -129,4 +130,25 @@ public static void parseDslExpressionAndCompare(ExpressionContext expressionCont
Exp actualExp = actualExpression.getResult().getExp();
assertEquals(exp == null ? null : Exp.build(exp), actualExp == null ? null : Exp.build(actualExp));
}
+
+ /**
+ * Parses the given DSL path String into array of {@link CTX}.
+ *
+ * @param pathToCtx String input representing DSL path
+ * @return The array of {@link CTX} or null
+ */
+ public static CTX[] parseCtx(String pathToCtx) {
+ return parser.parseCTX(pathToCtx);
+ }
+
+ /**
+ * Parses the given DSL path String and compares arrays of {@link CTX} using {@link CTX#toBase64(CTX[])} method.
+ *
+ * @param pathToCtx String input representing DSL path
+ * @param expected The array of {@link CTX} to be used for comparing
+ */
+ public static void parseCtxAndCompareAsBase64(String pathToCtx, CTX[] expected) {
+ CTX[] actualCtx = parser.parseCTX(pathToCtx);
+ assertEquals(expected == null ? null : CTX.toBase64(expected), actualCtx == null ? null : CTX.toBase64(actualCtx));
+ }
}