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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/guides/07-conditional-logic-control-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,45 @@ Expression filter = Exp.build(parsed.getResult().getExp());

The `when` structure enables you to push complex conditional logic directly to the server, reducing the need to pull data to the client for evaluation and minimizing data transfer.

## Error Handling: `unknown` and `error`

The `unknown` keyword produces an expression that throws a server-side exception whenever it is evaluated. While primarily useful in `when` branches to signal that a particular condition should never occur, `unknown`/`error` is syntactically valid in any expression position (e.g., `$.a == unknown`, `unknown + 1`). Outside a guarded `when` branch, it will cause a server-side exception on evaluation.

The `error` keyword is syntactic sugar for `unknown` -- both compile to the same underlying expression (`Exp.unknown()`). Use whichever reads better in your context: `error` may be clearer when the intent is to signal a failure, while `unknown` matches the underlying Aerospike Exp API name.

### Example: Fail on Default

Return a map bin if its size exceeds 5, otherwise throw a server-side exception:

```
when($.mapBin.{}.count() > 5 => $.mapBin, default => unknown)
```

Or equivalently with `error`:

```
when($.mapBin.{}.count() > 5 => $.mapBin, default => error)
```

### Example: Fail on Specific Branch

You can also use `unknown`/`error` in a non-default branch:

```
when($.status == 0 => error, $.status == 1 => 'active', default => 'inactive')
```

### Using as a Bin Name

Since `unknown` and `error` are reserved keywords, they are interpreted as expressions (not bin names) when used without the `$.` prefix. To reference bins named `unknown` or `error`, use the `$.` prefix:

```
$.unknown == 5
$.error == 10
```

Quoted forms are also supported and equivalent: `$.'unknown'`, `$."error"`.

## Control Structure `let`

The basic structure of a let expression allows you to declare temporary variables and use them within a subsequent expression:
Expand Down
23 changes: 17 additions & 6 deletions src/main/antlr4/com/aerospike/ael/Condition.g4
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ operand
| exclusiveExpression
| letExpression
| whenExpression
| unknownExpression
;

notExpression: 'not' '(' expression ')';
Expand All @@ -104,6 +105,8 @@ letExpression: 'let' '(' variableDefinition (',' variableDefinition)* ')' 'then'

whenExpression: 'when' '(' expressionMapping (',' expressionMapping)* ',' 'default' '=>' expression ')';

unknownExpression: 'unknown' | 'error';

functionCall
: NAME_IDENTIFIER '(' expression (',' expression)* ')'
;
Expand Down Expand Up @@ -233,12 +236,8 @@ PATH_FUNCTION_CDT_RETURN_TYPE
| 'REVERSE_RANK'
;

binPart
: BIN_IDENTIFIER
| NAME_IDENTIFIER
| QUOTED_STRING
| IN
| TRUE
reservedWord
: TRUE
| FALSE
| PATH_FUNCTION_GET
| PATH_FUNCTION_PARAM_TYPE
Expand All @@ -258,6 +257,16 @@ binPart
| 'increment'
| 'clear'
| 'sort'
| 'unknown'
| 'error'
;

binPart
: BIN_IDENTIFIER
| NAME_IDENTIFIER
| QUOTED_STRING
| IN
| reservedWord
;

mapPart
Expand Down Expand Up @@ -285,6 +294,7 @@ mapKey
| INT
| BLOB_LITERAL
| B64_LITERAL
| reservedWord
;

mapValue: '{=' valueIdentifier '}';
Expand Down Expand Up @@ -540,6 +550,7 @@ valueIdentifier
| IN
| BLOB_LITERAL
| B64_LITERAL
| reservedWord
;

valueListIdentifier: valueIdentifier ',' valueIdentifier (',' valueIdentifier)*;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/aerospike/ael/parts/AbstractPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum PartType {
VARIABLE_OPERAND,
PLACEHOLDER_OPERAND,
FUNCTION_ARGS,
BLOB_OPERAND
BLOB_OPERAND,
UNKNOWN_OPERAND
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.aerospike.ael.parts.controlstructure;

import com.aerospike.ael.parts.AbstractPart;
import com.aerospike.ael.parts.ExpressionContainer;
import lombok.Getter;

import java.util.List;

@Getter
public class AndStructure extends AbstractPart {

private final List<ExpressionContainer> operands;
private final List<AbstractPart> operands;

public AndStructure(List<ExpressionContainer> operands) {
public AndStructure(List<AbstractPart> operands) {
super(PartType.AND_STRUCTURE);
this.operands = operands;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.aerospike.ael.parts.controlstructure;

import com.aerospike.ael.parts.AbstractPart;
import com.aerospike.ael.parts.ExpressionContainer;
import lombok.Getter;

import java.util.List;

@Getter
public class ExclusiveStructure extends AbstractPart {

private final List<ExpressionContainer> operands;
private final List<AbstractPart> operands;

public ExclusiveStructure(List<ExpressionContainer> operands) {
public ExclusiveStructure(List<AbstractPart> operands) {
super(PartType.EXCLUSIVE_STRUCTURE);
this.operands = operands;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.aerospike.ael.parts.controlstructure;

import com.aerospike.ael.parts.AbstractPart;
import com.aerospike.ael.parts.ExpressionContainer;
import lombok.Getter;

import java.util.List;

@Getter
public class OrStructure extends AbstractPart {

private final List<ExpressionContainer> operands;
private final List<AbstractPart> operands;

public OrStructure(List<ExpressionContainer> operands) {
public OrStructure(List<AbstractPart> operands) {
super(PartType.OR_STRUCTURE);
this.operands = operands;
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/aerospike/ael/parts/operand/UnknownOperand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.aerospike.ael.parts.operand;

import com.aerospike.ael.client.exp.Exp;
import com.aerospike.ael.parts.AbstractPart;

/**
* AST node for the {@code unknown} and {@code error} keywords.
* Both compile to {@link Exp#unknown()}, which throws a server-side exception on evaluation.
*/
public class UnknownOperand extends AbstractPart {

public UnknownOperand() {
super(PartType.UNKNOWN_OPERAND);
}

@Override
public Exp getExp() {
return Exp.unknown();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/aerospike/ael/util/ParsingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ private static BigInteger parseUnsignedIntegerLiteral(String text) {
/**
* Resolves the string content from a parser rule context that may contain
* NAME_IDENTIFIER, QUOTED_STRING, or IN tokens.
* Rejects unquoted reserved words with a descriptive error.
*
* @param ctx Any parser rule context containing string-like tokens
* @return The resolved string, or {@code null} if no matching token is found
Expand All @@ -92,9 +93,21 @@ private static String resolveStringToken(ParserRuleContext ctx) {
if (in != null) {
return in.getText();
}
rejectUnquotedReservedWord(ctx);
return null;
}

private static void rejectUnquotedReservedWord(ParserRuleContext ctx) {
ConditionParser.ReservedWordContext rw = ctx.getRuleContext(
ConditionParser.ReservedWordContext.class, 0);
if (rw != null) {
String text = rw.getText();
throw new AelParseException(
"'%s' is a reserved word and must be quoted when used as a CDT part, e.g., $.<binName>.'%s' or $.<binName>.\"%s\""
.formatted(text, text, text));
}
}

/**
* Extracts a typed value from a {@code mapKey} parser rule context.
* Returns {@link Long} for all INT tokens (decimal, hex, binary),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,55 +97,60 @@ public AbstractPart visitAndExpression(ConditionParser.AndExpressionContext ctx)
return visit(ctx.comparisonExpression(0));
}

List<ExpressionContainer> expressions = new ArrayList<>();
List<AbstractPart> expressions = new ArrayList<>();
for (ConditionParser.ComparisonExpressionContext ec : ctx.comparisonExpression()) {
ExpressionContainer expr = (ExpressionContainer) visit(ec);
if (expr == null) return null;
AbstractPart part = visit(ec);
if (part == null) return null;

logicalSetBinAsBooleanExpr(expr);
expressions.add(expr);
if (part instanceof ExpressionContainer ec2) {
logicalSetBinAsBooleanExpr(ec2);
}
expressions.add(part);
}
return new ExpressionContainer(new AndStructure(expressions), ExpressionContainer.ExprPartsOperation.AND_STRUCTURE);
}

@Override
public AbstractPart visitOrExpression(ConditionParser.OrExpressionContext ctx) {
// If there's only one andExpression and no 'or' operators, just pass through
if (ctx.logicalAndExpression().size() == 1) {
return visit(ctx.logicalAndExpression(0));
}

List<ExpressionContainer> expressions = new ArrayList<>();
// iterate through each sub-expression
List<AbstractPart> expressions = new ArrayList<>();
for (ConditionParser.LogicalAndExpressionContext ec : ctx.logicalAndExpression()) {
ExpressionContainer expr = (ExpressionContainer) visit(ec);
if (expr == null) return null;
AbstractPart part = visit(ec);
if (part == null) return null;

logicalSetBinAsBooleanExpr(expr);
expressions.add(expr);
if (part instanceof ExpressionContainer ec2) {
logicalSetBinAsBooleanExpr(ec2);
}
expressions.add(part);
}
return new ExpressionContainer(new OrStructure(expressions), ExpressionContainer.ExprPartsOperation.OR_STRUCTURE);
}

@Override
public AbstractPart visitNotExpression(ConditionParser.NotExpressionContext ctx) {
ExpressionContainer expr = (ExpressionContainer) visit(ctx.expression());
AbstractPart part = visit(ctx.expression());

logicalSetBinAsBooleanExpr(expr);
return new ExpressionContainer(expr, ExpressionContainer.ExprPartsOperation.NOT);
if (part instanceof ExpressionContainer ec) {
logicalSetBinAsBooleanExpr(ec);
}
return new ExpressionContainer(part, ExpressionContainer.ExprPartsOperation.NOT);
}

@Override
public AbstractPart visitExclusiveExpression(ConditionParser.ExclusiveExpressionContext ctx) {
if (ctx.expression().size() < 2) {
throw new AelParseException("Exclusive logical operator requires 2 or more expressions");
}
List<ExpressionContainer> expressions = new ArrayList<>();
// iterate through each sub-expression
List<AbstractPart> expressions = new ArrayList<>();
for (ConditionParser.ExpressionContext ec : ctx.expression()) {
ExpressionContainer expr = (ExpressionContainer) visit(ec);
logicalSetBinAsBooleanExpr(expr);
expressions.add(expr);
AbstractPart part = visit(ec);
if (part instanceof ExpressionContainer ec2) {
logicalSetBinAsBooleanExpr(ec2);
}
expressions.add(part);
}
return new ExpressionContainer(new ExclusiveStructure(expressions),
ExpressionContainer.ExprPartsOperation.EXCLUSIVE_STRUCTURE);
Expand Down Expand Up @@ -513,7 +518,8 @@ private static void validateInVariableIsListCompatible(ExpressionContainer expr,
AbstractPart.PartType.STRING_OPERAND,
AbstractPart.PartType.MAP_OPERAND,
AbstractPart.PartType.METADATA_OPERAND,
AbstractPart.PartType.BLOB_OPERAND
AbstractPart.PartType.BLOB_OPERAND,
AbstractPart.PartType.UNKNOWN_OPERAND
);

private static boolean isNotList(AbstractPart part) {
Expand Down Expand Up @@ -858,8 +864,8 @@ public AbstractPart visitBinPart(ConditionParser.BinPartContext ctx) {
rejectBinNameContainingNull(binName);
return new BinPart(binName);
}
// Fallthrough: NAME_IDENTIFIER, IN, TRUE, FALSE, and all keyword literals ('and', 'or',
// 'not', etc.). ctx.getText() returns the matched text preserving original case.
// Fallthrough: NAME_IDENTIFIER, IN, and reservedWord alternatives.
// ctx.getText() returns the matched text preserving original case.
String binName = ctx.getText();
rejectBinNameContainingNull(binName);
return new BinPart(binName);
Expand All @@ -886,6 +892,7 @@ private static void rejectBinNameContainingNull(String binName) {
}
}


@Override
public AbstractPart visitOperandExpression(ConditionParser.OperandExpressionContext ctx) {
return visit(ctx.operand());
Expand Down Expand Up @@ -1024,6 +1031,11 @@ public AbstractPart visitBooleanOperand(ConditionParser.BooleanOperandContext ct
return new BooleanOperand(Boolean.parseBoolean(text));
}

@Override
public AbstractPart visitUnknownExpression(ConditionParser.UnknownExpressionContext ctx) {
return new UnknownOperand();
}

@Override
public AbstractPart visitPlaceholder(ConditionParser.PlaceholderContext ctx) {
// Extract index from the placeholder
Expand Down
Loading
Loading