Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ When modifying the grammar file you will need to re-generate the ANTLR sources b

`mvn clean generate-sources compile`

## CDT ranges

Map and list path segments support closed-boundaries and half-open ranges.
Semantics follow Aerospike CDT conventions (inclusive lower / exclusive upper for keys and values where applicable).
See [Guide: Working with Lists and Maps — CDT ranges](docs/guides/02-working-with-lists-and-maps.md#cdt-ranges).

## Usage examples

### Parsing AEL expression
Expand Down
10 changes: 10 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ Represents an available secondary index for optimization.
* Must be non-negative (`>= 0`).
* Providing a realistic, non-negative cardinality value is recommended for better automatic index selection (indexes with higher `binValuesRatio` are preferred).

## CDT path segments and ranges

Expressions may use **closed-boundaries** and **half-open CDT ranges** on maps and lists. Details in the
[Guide: Working with Lists and Maps — CDT ranges](guides/02-working-with-lists-and-maps.md#cdt-ranges).

The **`parseCTX`** API accepts the same surface syntax as paths embedded in expressions,
but **only simple selectors** produce `CTX[]` (single key, index, value, rank, etc.).
**Range segments** (index/value/key intervals) are not converted to context entries and will fail with an error
indicating that context is not supported for that part type.

## Path Functions

Path functions are suffixed to a bin or CDT path with dot notation.
Expand Down
10 changes: 10 additions & 0 deletions docs/guides/02-working-with-lists-and-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ Or use the full form of such AEL string if needed:
"$.user.{0}.get(type: STRING, return: VALUE) == 'Alice'"
```

#### CDT ranges

Beyond a single index, you can express **intervals** on CDT keys, values, indexes, and ranks using closed-boundaries
or half-open ranges:

- **Map key range** — `{a-c}` selects keys from `a` up to but not including `c`; `{a-}` is open on the upper side; `{-c}` is open on the lower side (all keys strictly below `c`).
- **Map index / rank range** — `{s:e}` selects index/rank span `[s, e)`; `{s:}` is open on the upper side; `{:e}` is open on the lower side (same span as `{0:e}`).
- **Map value range** — `{=v:w}`, `{=v:}`, `{=:w}` follow inclusive/exclusive value endpoints per `getByValueRange`.
- **Lists** — the same shapes use square brackets (`[…]`) instead of braces.

### Filtering by Map Value

**AEL String:**
Expand Down
8 changes: 7 additions & 1 deletion src/main/antlr4/com/aerospike/ael/Condition.g4
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ mapPart
| mapKey
| mapValue
| mapRank
| mapIndex
| mapKeyRange
| mapIndex
| mapKeyList
| mapIndexRange
| mapValueList
Expand Down Expand Up @@ -319,6 +319,7 @@ invertedMapKeyRange
keyRangeIdentifier
: mapKey '-' mapKey
| mapKey '-'
| '-' mapKey
;

mapKeyList
Expand Down Expand Up @@ -354,6 +355,7 @@ invertedMapIndexRange
indexRangeIdentifier
: start ':' end
| start ':'
| ':' end
;

signedInt: '-'? INT;
Expand Down Expand Up @@ -390,6 +392,7 @@ invertedMapValueRange
valueRangeIdentifier
: valueIdentifier ':' valueIdentifier
| valueIdentifier ':'
| ':' valueIdentifier
;

mapRankRange
Expand All @@ -408,6 +411,7 @@ invertedMapRankRange
rankRangeIdentifier
: start ':' end
| start ':'
| ':' end
;

mapRankRangeRelative
Expand All @@ -425,6 +429,7 @@ invertedMapRankRangeRelative

rankRangeRelativeIdentifier
: start ':' relativeRankEnd
| ':' relativeRankEnd
;

relativeRankEnd
Expand All @@ -451,6 +456,7 @@ invertedMapIndexRangeRelative

indexRangeRelativeIdentifier
: start ':' relativeKeyEnd
| ':' relativeKeyEnd
;

relativeKeyEnd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import com.aerospike.ael.client.exp.ListExp;
import com.aerospike.ael.parts.path.BasePath;

import static com.aerospike.ael.util.ParsingUtils.halfOpenRangeCount;
import static com.aerospike.ael.util.ParsingUtils.parseSignedInt;
import static com.aerospike.ael.util.ParsingUtils.subtractNullable;

public class ListIndexRange extends ListPart {
private final boolean isInverted;
Expand All @@ -20,7 +20,7 @@ public ListIndexRange(boolean isInverted, Integer start, Integer end) {
super(ListPartType.INDEX_RANGE);
this.isInverted = isInverted;
this.start = start;
this.count = subtractNullable(end, start);
this.count = halfOpenRangeCount(start, end);
}

public static ListIndexRange from(ConditionParser.ListIndexRangeContext ctx) {
Expand All @@ -32,11 +32,8 @@ public static ListIndexRange from(ConditionParser.ListIndexRangeContext ctx) {
indexRange != null ? indexRange.indexRangeIdentifier() : invertedIndexRange.indexRangeIdentifier();
boolean isInverted = indexRange == null;

Integer start = parseSignedInt(range.start().signedInt());
Integer end = null;
if (range.end() != null) {
end = parseSignedInt(range.end().signedInt());
}
Integer start = range.start() != null ? parseSignedInt(range.start().signedInt()) : null;
Integer end = range.end() != null ? parseSignedInt(range.end().signedInt()) : null;

return new ListIndexRange(isInverted, start, end);
}
Expand All @@ -49,7 +46,8 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | ListReturnType.INVERTED;
}

Exp startExp = Exp.val(start);
int startIndex = start != null ? start : 0;
Exp startExp = Exp.val(startIndex);
if (count == null) {
return ListExp.getByIndexRange(cdtReturnType, startExp, Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import com.aerospike.ael.client.exp.ListExp;
import com.aerospike.ael.parts.path.BasePath;

import static com.aerospike.ael.util.ParsingUtils.halfOpenRangeCount;
import static com.aerospike.ael.util.ParsingUtils.parseSignedInt;
import static com.aerospike.ael.util.ParsingUtils.subtractNullable;

public class ListRankRange extends ListPart {
private final boolean isInverted;
Expand All @@ -20,7 +20,7 @@ public ListRankRange(boolean isInverted, Integer start, Integer end) {
super(ListPartType.RANK_RANGE);
this.isInverted = isInverted;
this.start = start;
this.count = subtractNullable(end, start);
this.count = halfOpenRangeCount(start, end);
}

public static ListRankRange from(ConditionParser.ListRankRangeContext ctx) {
Expand All @@ -32,11 +32,8 @@ public static ListRankRange from(ConditionParser.ListRankRangeContext ctx) {
rankRange != null ? rankRange.rankRangeIdentifier() : invertedRankRange.rankRangeIdentifier();
boolean isInverted = rankRange == null;

Integer start = parseSignedInt(range.start().signedInt());
Integer end = null;
if (range.end() != null) {
end = parseSignedInt(range.end().signedInt());
}
Integer start = range.start() != null ? parseSignedInt(range.start().signedInt()) : null;
Integer end = range.end() != null ? parseSignedInt(range.end().signedInt()) : null;

return new ListRankRange(isInverted, start, end);
}
Expand All @@ -49,7 +46,8 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | ListReturnType.INVERTED;
}

Exp startExp = Exp.val(start);
int startRank = start != null ? start : 0;
Exp startExp = Exp.val(startRank);
if (count == null) {
return ListExp.getByRankRange(cdtReturnType, startExp, Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import com.aerospike.ael.parts.path.BasePath;
import com.aerospike.ael.util.ParsingUtils;

import static com.aerospike.ael.util.ParsingUtils.halfOpenRangeCount;
import static com.aerospike.ael.util.ParsingUtils.objectToExp;
import static com.aerospike.ael.util.ParsingUtils.parseSignedInt;
import static com.aerospike.ael.util.ParsingUtils.subtractNullable;

public class ListRankRangeRelative extends ListPart {
private final boolean isInverted;
Expand All @@ -23,7 +23,7 @@ public ListRankRangeRelative(boolean isInverted, Integer start, Integer end, Obj
super(ListPartType.RANK_RANGE_RELATIVE);
this.isInverted = isInverted;
this.start = start;
this.count = subtractNullable(end, start);
this.count = halfOpenRangeCount(start, end);
this.relative = relative;
}

Expand All @@ -37,7 +37,7 @@ public static ListRankRangeRelative from(ConditionParser.ListRankRangeRelativeCo
: invertedRankRangeRelative.rankRangeRelativeIdentifier();
boolean isInverted = rankRangeRelative == null;

Integer start = parseSignedInt(range.start().signedInt());
Integer start = range.start() != null ? parseSignedInt(range.start().signedInt()) : null;
Integer end = null;
if (range.relativeRankEnd().end() != null) {
end = parseSignedInt(range.relativeRankEnd().end().signedInt());
Expand All @@ -62,14 +62,14 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType

Exp relativeExp = objectToExp(relative);

Exp startExp = Exp.val(start);
Exp valueExp = Exp.val(start != null ? start : 0);
if (count == null) {
return ListExp.getByValueRelativeRankRange(cdtReturnType, startExp, relativeExp,
return ListExp.getByValueRelativeRankRange(cdtReturnType, valueExp, relativeExp,
Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
}

return ListExp.getByValueRelativeRankRange(cdtReturnType, startExp, relativeExp, Exp.val(count),
return ListExp.getByValueRelativeRankRange(cdtReturnType, valueExp, relativeExp, Exp.val(count),
Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
}
Expand Down
34 changes: 20 additions & 14 deletions src/main/java/com/aerospike/ael/parts/cdt/list/ListValueRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@
import com.aerospike.ael.client.exp.ListExp;
import com.aerospike.ael.parts.path.BasePath;

import static com.aerospike.ael.util.ParsingUtils.requireIntValueIdentifier;
import com.aerospike.ael.util.ParsingUtils;

import static com.aerospike.ael.util.ParsingUtils.objectToExp;
import static com.aerospike.ael.util.ParsingUtils.requireSupportedExpValue;

public class ListValueRange extends ListPart {
private final boolean isInverted;
private final Integer start;
private final Integer end;
private final Object start;
private final Object end;

public ListValueRange(boolean isInverted, Integer start, Integer end) {
public ListValueRange(boolean isInverted, Object start, Object end) {
super(ListPartType.VALUE_RANGE);
if (start != null) {
requireSupportedExpValue(start, "ListValueRange start");
}
if (end != null) {
requireSupportedExpValue(end, "ListValueRange end");
}
if (start == null && end == null) {
throw new AelParseException("ListValueRange requires at least one bound");
}
this.isInverted = isInverted;
this.start = start;
this.end = end;
Expand All @@ -31,14 +43,8 @@ public static ListValueRange from(ConditionParser.ListValueRangeContext ctx) {
valueRange != null ? valueRange.valueRangeIdentifier() : invertedValueRange.valueRangeIdentifier();
boolean isInverted = valueRange == null;

Integer startValue = requireIntValueIdentifier(range.valueIdentifier(0));

Integer endValue = null;
if (range.valueIdentifier(1) != null) {
endValue = requireIntValueIdentifier(range.valueIdentifier(1));
}

return new ListValueRange(isInverted, startValue, endValue);
ParsingUtils.NullableEndpoints<Object> bounds = ParsingUtils.parseValueRangeEndpoints(range);
return new ListValueRange(isInverted, bounds.start(), bounds.end());
}
throw new AelParseException("Could not translate ListValueRange from ctx: %s".formatted(ctx));
}
Expand All @@ -49,8 +55,8 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | ListReturnType.INVERTED;
}

Exp startExp = Exp.val(start);
Exp endExp = end != null ? Exp.val(end) : null;
Exp startExp = start != null ? objectToExp(start) : null;
Exp endExp = end != null ? objectToExp(end) : null;

return ListExp.getByValueRange(cdtReturnType, startExp, endExp, Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
Expand Down
14 changes: 6 additions & 8 deletions src/main/java/com/aerospike/ael/parts/cdt/map/MapIndexRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import com.aerospike.ael.client.exp.MapExp;
import com.aerospike.ael.parts.path.BasePath;

import static com.aerospike.ael.util.ParsingUtils.halfOpenRangeCount;
import static com.aerospike.ael.util.ParsingUtils.parseSignedInt;
import static com.aerospike.ael.util.ParsingUtils.subtractNullable;

public class MapIndexRange extends MapPart {
private final boolean isInverted;
Expand All @@ -20,7 +20,7 @@ public MapIndexRange(boolean isInverted, Integer start, Integer end) {
super(MapPartType.INDEX_RANGE);
this.isInverted = isInverted;
this.start = start;
this.count = subtractNullable(end, start);
this.count = halfOpenRangeCount(start, end);
}

public static MapIndexRange from(ConditionParser.MapIndexRangeContext ctx) {
Expand All @@ -32,11 +32,8 @@ public static MapIndexRange from(ConditionParser.MapIndexRangeContext ctx) {
indexRange != null ? indexRange.indexRangeIdentifier() : invertedIndexRange.indexRangeIdentifier();
boolean isInverted = indexRange == null;

Integer start = parseSignedInt(range.start().signedInt());
Integer end = null;
if (range.end() != null) {
end = parseSignedInt(range.end().signedInt());
}
Integer start = range.start() != null ? parseSignedInt(range.start().signedInt()) : null;
Integer end = range.end() != null ? parseSignedInt(range.end().signedInt()) : null;

return new MapIndexRange(isInverted, start, end);
}
Expand All @@ -49,7 +46,8 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | MapReturnType.INVERTED;
}

Exp startExp = Exp.val(start);
int startIndex = start != null ? start : 0;
Exp startExp = Exp.val(startIndex);
if (count == null) {
return MapExp.getByIndexRange(cdtReturnType, startExp, Exp.bin(basePath.getBinPart().getBinName(),
basePath.getBinType()), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import com.aerospike.ael.util.ParsingUtils;

import static com.aerospike.ael.util.ParsingUtils.halfOpenRangeCount;
import static com.aerospike.ael.util.ParsingUtils.parseSignedInt;
import static com.aerospike.ael.util.ParsingUtils.subtractNullable;

public class MapIndexRangeRelative extends MapPart {
private final boolean isInverted;
Expand All @@ -26,7 +26,7 @@ public MapIndexRangeRelative(boolean isInverted, Integer start, Integer end, Obj
}
this.isInverted = isInverted;
this.start = start;
this.count = subtractNullable(end, start);
this.count = halfOpenRangeCount(start, end);
this.relative = relative;
}

Expand All @@ -40,7 +40,7 @@ public static MapIndexRangeRelative from(ConditionParser.MapIndexRangeRelativeCo
: invertedIndexRangeRelative.indexRangeRelativeIdentifier();
boolean isInverted = indexRangeRelative == null;

Integer start = parseSignedInt(range.start().signedInt());
Integer start = range.start() != null ? parseSignedInt(range.start().signedInt()) : null;
Integer end = null;
if (range.relativeKeyEnd().end() != null) {
end = parseSignedInt(range.relativeKeyEnd().end().signedInt());
Expand All @@ -62,7 +62,8 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
}

Exp keyExp = ParsingUtils.objectToExp(relative);
Exp startExp = Exp.val(start);
int startIndex = start != null ? start : 0;
Exp startExp = Exp.val(startIndex);
if (count == null) {
return MapExp.getByKeyRelativeIndexRange(cdtReturnType, keyExp, startExp,
Exp.bin(basePath.getBinPart().getBinName(),
Expand Down
Loading
Loading