AEL (Aerospike Expression Language) reference
AEL is a text-based DSL that compiles to Aerospike server expressions. It is
used for filter expressions (.where()) and as the source of read/write
expressions in both Java and Python:
.selectFrom() / .select_from(), .insertFrom() / .insert_from(),
.updateFrom() / .update_from(), and .upsertFrom() / .upsert_from().
Full AEL syntax reference:
docs/ael-documentation.md
Using AEL in the SDK
Filter expressions with .where()
session.query(users) .where("$.age > 21 and $.status == 'active'") .execute();stream = await session.query(users).where("$.age > 21 and $.status == 'active'").execute()# stream.close() is synchronous (not await)Single-key and batch operations:
session.update(users.id("u1")) .bin("lastSeen").setTo(System.currentTimeMillis()) .where("$.status == 'active'") .execute();import time
await ( session.update(users.id("u1")) .bin("lastSeen").set_to(int(time.time() * 1000)) .where("$.status == 'active'") .execute())Parameterized expressions with PreparedAel
Avoid string concatenation by using placeholders.
Java uses $1, $2, … and Python uses ?0, ?1, … in the AEL string.
PreparedAel prepared = new PreparedAel("$.age > $1 and $.name == $2");
session.query(users) .where(prepared, 21, "Tim") .execute();from aerospike_sdk import parse_ael
# Parameterized expression (?0, ?1, ... in the string; values are bound positionally)expr = parse_ael("$.age > ?0 and $.name == ?1", 21, "Tim")stream = await session.query(users).where(expr).execute()Java placeholders are 1-based in the string and 0-based in the params array; Python placeholders are 0-based in both the string and the bound values.
Quick syntax reference
Record paths
Every path starts with $:
$.binName -- scalar bin$.profile.name -- map key access$.scores.[0] -- list index access$.data.users.[2].name -- deeply nestedBin type inference
The first context element determines the bin type:
| Path | Inferred type |
|---|---|
$.x.name | Map (first context is identifier) |
$.x.[0] | List (first context is [) |
$.x > 5 | Scalar (no context) |
Comparison operators
$.age > 21$.name == 'Tim'$.price >= 100.0$.status != 'inactive'Both sides can be bins (requires explicit type):
$.binA.get(type: INT) > $.binB.get(type: INT)The in operator
$.name in ["Bob", "Mary", "Richard"]"gold" in $.allowedStatusesLogical operators
$.age > 21 and $.status == 'active'$.role == 'admin' or $.role == 'super'not($.age > 65)exclusive($.a > 10, $.b > 5)and binds tighter than or. Use parentheses to clarify:
$.a > 1 and ($.b > 2 or $.c > 3)Arithmetic
($.price * $.qty) > 1000($.apples + 5) > 10abs($.score - 50) < 10max($.a, $.b, $.c) > 100Both operands must be the same type. Use asInt() / asFloat() to convert:
$.intBin + $.floatBin.asInt()Record metadata
$.ttl() < 3600 -- expires in < 1 hour$.recordSize() > 1024 -- large records$.sinceUpdate() < 7200000 -- updated in last 2 hours$.isTombstone() -- deleted records$.setName() == 'critical'$.digestModulo(3) == 0 -- partition sampling| Function | Returns | Description |
|---|---|---|
$.ttl() | INT | Time-to-live in seconds |
$.voidTime() | INT | Absolute expiry (-1 = never) |
$.lastUpdate() | INT | Last update (ns since epoch) |
$.sinceUpdate() | INT | Ms since last update |
$.setName() | STRING | Record’s set name |
$.keyExists() | BOOL | Whether user key is stored |
$.isTombstone() | BOOL | Whether record is deleted |
$.recordSize() | INT | Total size in bytes |
$.deviceSize() | INT | Storage size on device |
$.memorySize() | INT | Size in memory |
$.digestModulo(n) | INT | Digest modulo n |
CDT path patterns
Map access
$.profile.name -- string key$.profile.'special-key' -- quoted string key$.m.1 -- integer key$.m.{1} -- map by index$.m.{=bb} -- map by value$.m.{#1} -- map by rankList access
$.scores.[0] -- by index$.scores.[-1] -- last element$.scores.[=42] -- by value$.scores.[#0] -- by rank (lowest)Plural selectors (leaf only)
$.m.{a-d} -- key range [a, d)$.m.{a,b,c} -- key list$.m.{0:3} -- index range$.m.{=10:20} -- value range$.m.{#-3:} -- top 3 by rank$.l.[1:5] -- list index range$.l.[=1,2,3] -- list value list$.l.[#0:3] -- list rank rangeInverted selections (prefix !)
$.m.{!a-d} -- everything except keys a-c$.l.[!0:3] -- everything except indices 0-2$.m.{!=temp,draft} -- everything except these keysPath functions
get() — explicit type and return
$.binName.get(type: INT)$.mapBin.{a,b}.get(return: KEY_VALUE)$.listBin.[0:3].get(return: COUNT)| Parameter | Values |
|---|---|
type | INT, STRING, FLOAT, BOOL, BLOB, HLL, LIST, MAP, GEO |
return | VALUE, COUNT, INDEX, RANK, NONE, EXISTS, KEY, KEY_VALUE, ORDERED_MAP, UNORDERED_MAP |
count() — element count
$.listBin.[].count() -- list size$.mapBin.{}.count() -- map size$.listBin.[=4].count() > 0 -- count of elements equal to 4exists() — existence check
$.binA.exists() and $.binB.exists()$.mapBin.address.exists()Control structures
Conditional: when
when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze')default clause is mandatory.
Variable binding: let ... then
let (total = $.price * $.qty, tax = ${total} * 0.1) then (${total} + ${tax})Variables reference earlier variables with ${name}.
Read expressions with selectFrom
selectFrom evaluates an AEL expression server-side and returns the result as a virtual bin. The bin doesn’t need to exist — the expression can reference any bins on the record.
Record rec = session.query(users.id("u1")) .bin("total").selectFrom("$.price * $.quantity") .bin("ageIn10Years").selectFrom("$.age + 10") .execute() .getFirstRecord();
long total = rec.getLong("total");long futureAge = rec.getLong("ageIn10Years");stream = await ( session.query(users.id("u1")) .bin("total").select_from("$.price * $.quantity") .bin("ageIn10Years").select_from("$.age + 10") .execute())first = await stream.first_or_raise()total = first.record.bins["total"]stream.close()The result bin name is whatever you pass to .bin(...) — it doesn’t need to match any existing bin. Use ignoreEvalFailure() to skip records where the expression can’t evaluate:
session.query(users) .bin("ratio").selectFrom("$.a / $.b", opt -> opt.ignoreEvalFailure()) .execute();Write expressions with insertFrom, updateFrom, upsertFrom
These evaluate an AEL expression server-side and write the result into a bin, with different existence semantics:
| Method | Behavior |
|---|---|
upsertFrom(ael) | Creates or overwrites the bin |
insertFrom(ael) | Creates only — fails with BIN_EXISTS_ERROR if bin exists |
updateFrom(ael) | Updates only — fails with BIN_NOT_FOUND if bin doesn’t exist |
session.upsert(users.id("u1")) .bin("total").upsertFrom("$.price * $.quantity") .bin("discount").insertFrom("$.coupon * 0.1") .execute();await ( session.upsert(users.id("u1")) .bin("total").upsert_from("$.price * $.quantity") .bin("discount").insert_from("$.coupon * 0.1") .execute())Options for write expressions:
session.upsert(users.id("u1")) .bin("discount").upsertFrom("$.coupon", opt -> opt .deleteIfNull() // delete the bin if expression returns null .ignoreEvalFailure()) // don't fail if expression can't evaluate .bin("bonus").insertFrom("$.base * 1.5", opt -> opt .ignoreOpFailure()) // don't fail if bin already exists .execute();Where clause integration patterns
On dataset queries
session.query(users) .where("$.age > 21") .execute();stream = await session.query(users).where("$.age > 21").execute()On single-key / batch operations
session.update(users.ids("u1", "u2")) .bin("bonus").add(100) .where("$.department == 'engineering'") .execute();await ( session.update(users.ids("u1", "u2")) .bin("bonus").add(100) .where("$.department == 'engineering'") .execute())On mixed batch chains (per-operation and default)
session .update(users.id("u1")).bin("bonus").add(100) .where("$.department == 'engineering'") .update(users.id("u2")).bin("bonus").add(50) .defaultWhere("$.active == true") .execute();await ( session.update(users.id("u1")).bin("bonus").add(100).where("$.department == 'engineering'") .update(users.id("u2")).bin("bonus").add(50) .default_where("$.active == true") .execute())With PreparedAel for reuse
PreparedAel activeInDept = new PreparedAel("$.active == true and $.department == $1");
session.query(users) .where(activeInDept, "engineering") .execute();
session.query(users) .where(activeInDept, "marketing") .execute();from aerospike_sdk import parse_ael
# Same template string; bind parameters per query via parse_ael(...)expr_eng = parse_ael("$.active == true and $.department == ?0", "engineering")stream_eng = await session.query(users).where(expr_eng).execute()stream_eng.close() # sync, not await
expr_mkt = parse_ael("$.active == true and $.department == ?0", "marketing")stream_mkt = await session.query(users).where(expr_mkt).execute()stream_mkt.close()