AEL control structures and postfix flags
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
Reference page: part of the AEL reference. Covers when/let control structures and the postfix flags that modify write and read terminals. See Applies to on the overview page for SDK and Database version requirements.
Conditional: when
when works like an IF … THEN … ELSIF … ELSE chain in other languages: it evaluates each condition in order and returns the value paired with the first one that’s true.
when (cond1 => val1, cond2 => val2, default => val3)| Part | Requirement |
|---|---|
Conditions (condN) | TRILEAN |
Actions (valN) | All branches the same type, except the reserved literals unknown and error (explained in the next paragraph) |
default => … | Required fallback branch |
Result type is the unified action type of all non-unknown / non-error branches.
when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze')Reserved trilean value literals: the words unknown and error are value literals, not exceptions, parse failures, or shorthand for “throw”. Both spellings are synonymous: they may appear on any branch regardless of the types on other branches, and at evaluation time both produce the TRILEAN value unknown. Typical use: default => unknown (or default => error) when no condition matches and the expression should yield an indeterminate result rather than a typed default. This is distinct from a condition or comparison returning unknown because a bin or path operand is absent (see TRILEAN).
/* Parse error — branch result types must agree (INT default vs STRING arms) */when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', $.tier == 3 => 'bronze', default => 0)
/* Valid — `unknown` and `error` are interchangeable value literals */when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', $.tier == 3 => 'bronze', default => unknown /* same runtime value as `default => error` */)Variable binding: let, then
let (var1 = expr1, var2 = expr2) then (bodyExpr)References to bound variables use ${varName}. Variable types are inferred from their initializer expressions. Variables can reference earlier variables in the same binding list:
let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)Float literals in let expressions cause a parse error.
Use only integer arithmetic in let expressions unless all operands are explicitly floats:
/* Parse error (cannot compare: FLOAT vs INT): INT combined with a float literal */let (total = $.price * $.qty, tax = ${total} * 0.1) then (${total} + ${tax} > 900)
/* Parse error (type mismatch: INT vs FLOAT): INT bin multiplied by a float literal */$.intBin:INT * 0.1 > 5
/* Fixed: keep everything INT (scale by 1000 or use integer percentages) */let (total = $.price * $.qty, tax_pct = 10, tax = ${total} / ${tax_pct}) then (${total} + ${tax} > 990)Type consistency in let bindings
To avoid this:
- Pin every bin used in
letarithmetic to an explicit type (:INTor:FLOAT), or - Use
.toFloat()/.toInt()explicitly to cast before combining.
(${name}).toFloat() / (${name}).toInt() follow the same explicit-cast
rules as casting a bin path (see Types and type suffixes).
A let variable reference cannot have functions called on it, so it must be
parenthesized before calling a cast method: ${total}.toFloat() is a syntax
error, but (${total}).toFloat() compiles.
/* All INT arithmetic: */let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)
/* Mixed types (NOTE: INT * FLOAT silently excludes every record in a filter expression, but is a runtime error in a read/write expression, if types don't align): */let (discount_rate = 0.13, total = $.price * $.qty, discount = ${total} * $.discount_rate) then (${total} - ${discount} > 400)
/* Fixed with explicit casts and pinned types: */let (total = $.price:INT * $.qty, discount = (${total}).toFloat() * $.discount_rate:FLOAT) then ((${total}).toFloat() - ${discount} > 400.0)Postfix flags
Attach postfix flags immediately after ) on path terminals, not as named parameters inside ():
| Flag | Valid on | Description |
|---|---|---|
:NO_FAIL | Collection data type (CDT) writes; modify(), remove(); pathed string modify; hllInit, hllAdd; all BLOB bit modify ops | Absent-path / policy tolerance — see :NO_FAIL semantics |
:PARTIAL | Map putItems, insertItems, updateItems; list appendItems, insertItems (index path); BLOB bitRemove, bitSet, bitOr, bitXor, bitAnd, bitNot, bitLshift, bitRshift | On bulk CDT writes: apply entries that succeed even when others fail; implies :NO_FAIL. On BLOB bit ops: clip the op to the blob end when the range extends past the end; does not imply :NO_FAIL |
:CREATE_ONLY | hllInit, hllAdd; bitResize, bitInsert | Fail if the operation would modify an existing bin |
:UPDATE_ONLY | hllInit; all BLOB bit modify ops | Fail if the operation would create a new bin. Parse error on hllAdd |
:ADD_UNIQUE | List append, appendItems, insert, insertItems, setTo, add | Fail (or skip under :NO_FAIL) when an element equals one already in the list. A duplicate within the same bulk payload reports OP_NOT_APPLICABLE; a duplicate against existing list content reports ELEMENT_EXISTS |
:DROP_DUPS | sort() | Drop duplicate elements while sorting |
:REVERSE | getIndexes(), getRanks() | Reverse index/rank direction |
:UNORDERED | getMaps() | Unordered return map shape on getMaps() only — distinct from the map literal and path-segment create-order uses of :UNORDERED; see Literals |
:PERSIST_INDEX | Bin root only | Persist top-level map index on create |
:UNSORTED_PAD | Path segment that materializes a missing list — not valid on modify() or remove() paths | Create an unsorted list with elements allowed to be inserted anywhere past the end of the list when materializing a missing container; see Bounded list writes |
Terminal-kind rule (create-order vs. :NO_FAIL vs. reads): create-order suffixes are write-only and allowed only on write terminals that can create containers. :NO_FAIL is write-only. Read terminals take neither create-order suffixes nor :NO_FAIL. :UNSORTED_PAD is a create-order suffix.
Read paths when a segment is absent: reads cannot use :NO_FAIL: a missing CDT context step fails under strict navigation (see Record and bin prefix). Use exists() to test presence instead — it returns false when the path does not match.
/* :REVERSE reverses the index/rank direction of the returned list */$.scores:LIST.[1:3].getIndexes():REVERSE
/* :PERSIST_INDEX persists the top-level map's key index so subsequent key-based lookups on this bin can use it, instead of rebuilding it each time. It's valid on the bin root only, so it must come before any path segments. */$.m:KEY_ORDERED:PERSIST_INDEX.k.setTo(1)$.optional.field.setTo('value'):NO_FAIL$.tags.append('x'):ADD_UNIQUE$.tags.sort():DROP_DUPS$.m.{@a: d}.getMaps():UNORDERED$.m:MAP.insertItems({a: 1, b: 2}):PARTIAL$.h.hllInit(indexBits: 14):CREATE_ONLY:PARTIAL trades all-or-nothing atomicity for tolerance of individual failures within a bulk write. For example, consider using a list as a set with :ADD_UNIQUE. To add several candidate elements where some may already be present, $.tags.appendItems(candidates):ADD_UNIQUE:PARTIAL inserts only the elements that aren’t already in the list and skips the duplicates, instead of failing the whole call on the first one it finds. Without :PARTIAL or :NO_FAIL, a bulk write fails atomically on the first error, which is the right choice when your application requires all-or-nothing semantics. :NO_FAILis implied by:PARTIAL` in this context.
:NO_FAIL semantics
The flag has two runtime axes; both are narrower than “suppress any failure”:
| Axis | Valid on | Effect when set |
|---|---|---|
| Path-level (absent CTX) | CDT writes; modify(), remove(); pathed string modify | A CDT context segment on the compiled path is missing in the bin → no-op; the original bin is left unchanged |
| Bin-level (create/update policy) | hllInit, hllAdd; BLOB bit modify ops | A :CREATE_ONLY / :UPDATE_ONLY (or related type) conflict on the bin → no-op instead of failing |
:NO_FAIL does not:
- Suppress parse errors (including invalid flag placement on a terminal — AEL rejects nonsensical postfix at compile time).
- Suppress read-terminal failures.
- Suppress op-specific failures unless a separate mechanism applies (for example BLOB
:PARTIALclips a range).
:PARTIAL and :NO_FAIL: on bulk CDT ops (putItems, insertItems, updateItems, appendItems, list insertItems), :PARTIAL requires tolerant failure and automatically applies :NO_FAIL at AEL compile time. Without :PARTIAL, bulk CDT ops fail atomically (one failed entry aborts the entire call). On BLOB bit modify ops, :PARTIAL and :NO_FAIL are independent: :PARTIAL clips to the blob end, and :NO_FAIL suppresses create/update flag conflicts. Write both on BLOB when needed, for example bitSet(…):PARTIAL:NO_FAIL. On bulk CDT, explicit :NO_FAIL with :PARTIAL is redundant.
:NO_FAIL restrictions, summarized:
- Valid only on writes, never on read terminals.
- Applies to: absent-context container creation on a write path (such as the earlier
$.optional.fieldexample, whereoptionaldoesn’t yet exist),BLOB/HLLbin create/update policy, and pathedSTRINGmodify functions (not a bare bin or(expr)receiver). - Does not mean “suppress any failure” — it tolerates one specific failure condition for its context.
The Developer SDK splits :NO_FAIL across the same two axes, as separate builder options on operation-expression builders: the path-level axis (absent CDT context, an expression resolving to unknown or a non-bin type) maps to ignoreEvalFailure() / ignore_eval_failure=True, and the bin-level axis (a :CREATE_ONLY / :UPDATE_ONLY create/update policy conflict) maps to ignoreOpFailure() / ignore_op_failure=True. The two are independent and can be combined on the same operation, for example opt -> opt.ignoreOpFailure().ignoreEvalFailure() in Java.
Next steps
- Type inference and limits — compile limits and name-collision disambiguation
- String, BLOB, and HLL functions — per-op flag validity tables for BLOB and HLL
- Functions and terminals — path write terminals that accept these flags