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)); + } }