Skip to content

AEL reference

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

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().

Using AEL in the SDK

Filter expressions with .where()

session.query(users)
.where("$.age > 21 and $.status == 'active'")
.execute();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute()

stream = await session.query(users).where("$.age > 21 and $.status == 'active'").execute()
# stream.close() is synchronous (not await)

📖 API reference: Session.query() | QueryBuilder.where() | RecordStream.close() | QueryBuilder.execute()

Single-key and batch operations:

session.update(users.id("u1"))
.bin("lastSeen").setTo(System.currentTimeMillis())
.where("$.status == 'active'")
.execute();

📖 API reference: DataSet.id(...) | Session.update(DataSet) | ChainableOperationBuilder.bin(...) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute()

import time
await (
session.update(users.id("u1"))
.bin("lastSeen").set_to(int(time.time() * 1000))
.where("$.status == 'active'")
.execute()
)

📖 API reference: DataSet.id() | Session.update() | QueryBuilder.where() | QueryBuilder.bin() | WriteSegmentBuilder.set_to() | WriteSegmentBuilder.bin() | WriteSegmentBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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()

📖 API reference: Session.query() | QueryBuilder.where() | QueryBuilder.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 nested

Bin type inference

The first context element determines the bin type:

PathInferred type
$.x.nameMap (first context is identifier)
$.x.[0]List (first context is [)
$.x > 5Scalar (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 $.allowedStatuses

Logical 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) > 10
abs($.score - 50) < 10
max($.a, $.b, $.c) > 100
$.value % 2 == 0
$.base ** 2 > 100

Both operands must be the same type. Use asInt() / asFloat() to convert between numeric types, or to cast a numerically-valued bin before comparison:

$.intBin + $.floatBin.asInt()
$.count.asFloat() > 3.14
3.14.asInt() == 3

asInt() / asFloat() converts between the two numeric types (INTFLOAT). It does not parse string-typed bins. A bin stored as the string "42" cannot be compared numerically with .asInt(). Store the value as an integer-typed bin instead.

Integer division and %: When both operands of / are INT bins, Aerospike performs integer division only when the result is exact. A division that produces a remainder (such as 460 / 3) is evaluated as a FLOAT. Because % (modulo) requires integer operands on both sides, using % after a non-exact division raises ParameterError on the server. To stay safe with %, either:

  • design fixture data so all divisions are exact (such as 10 / 2, not 10 / 3), or
  • cast back with .asInt() before applying %: ($.a / $.b).asInt() % 10.

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
FunctionReturnsDescription
$.ttl()INTTime-to-live in seconds
$.voidTime()INTAbsolute expiry (-1 = never)
$.lastUpdate()INTLast update (ns since epoch)
$.sinceUpdate()INTMs since last update
$.setName()STRINGRecord’s set name
$.keyExists()BOOLWhether user key is stored
$.isTombstone()BOOLWhether record is deleted
$.recordSize()INTTotal size in bytes
$.deviceSize()INTStorage size on device
$.memorySize()INTSize in memory
$.digestModulo(n)INTDigest modulo n

CDT path patterns

Map access

$.profile.name -- string key (dot notation)
$.profile.["name"] -- string key (bracket notation, equivalent)
$.profile.'special-key' -- quoted string key (dot notation, for keys with special chars)
$.profile.["special-key"] -- bracket notation alternative
$.m.1 -- integer key
$.m.{1} -- map by index
$.m.{=bb} -- map by value
$.m.{#1} -- map by rank

Reserved keywords require bracket notation. Some identifiers are reserved in the AEL grammar (type, count, exists, get, in, and, or, not, etc.). Using them as map key names with dot notation causes an AelParseException. Always use bracket notation for such keys:

-- Fails if 'type' is a reserved word:
$.metadata.type == 'fixture'
-- Correct: bracket notation bypasses keyword parsing
$.metadata.["type"] == 'fixture'

List 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) ← dash separates KEY range bounds
$.m.{a,b,c} -- key list
$.m.{0:3} -- index range ← colon separates INDEX range bounds
$.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 range

Key range vs index range: do not confuse the delimiters. {a-d} uses a dash (-) and selects by key (string or integer keys in lexicographic/numeric order). {0:3} uses a colon (:) and selects by position index. Using : with string bounds like {room1:room3} is a parse error; the correct form is {room1-room3}.

-- String key range from "room1" up to (but not including) "room3":
$.rooms.{room1-room3}
-- Count of entries in that range:
$.rooms.{room1-room3}.count()
-- WRONG: colon is for index ranges, not key ranges:
$.rooms.{room1:room3} ← parse error

Inverted selections (prefix !)

$.m.{!a-d} -- everything except keys a-c
$.l.[!0:3] -- everything except indices 0-2
$.m.{!=temp,draft} -- everything except these keys

Path functions

get() — explicit type and return

$.binName.get(type: INT)
$.mapBin.{a,b}.get(return: KEY_VALUE)
$.listBin.[0:3].get(return: COUNT)
ParameterValues
typeINT, STRING, FLOAT, BOOL, BLOB, HLL, LIST, MAP, GEO
returnVALUE, 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 4

exists() — 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, discount = ${total} / 10) then (${total} - ${discount} > 400)

Variables reference earlier variables with ${name}.

Float literals in let expressions cause silent failures

If any value in a let binding is a float literal (such as 0.1, 1.5) or produces a FLOAT result, but the bin it is combined with is INT, the expression silently returns no matches. There is no parse error. This also applies to ${var} * 0.1 where ${var} holds an integer.

Use only integer arithmetic in let expressions unless all operands are explicitly floats.

-- Safe: all INT arithmetic
let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)
-- Silent failure: INT * float literal
let (total = $.price * $.qty, tax = ${total} * 0.1) then (${total} + ${tax} > 900)
-- 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. All arithmetic within a let expression must use consistent types. Mixing INT and FLOAT values in the same arithmetic expression (such as ${total} is INT but $.discount_rate is FLOAT) causes the expression to silently return no matches. There is no parse error, but the filter produces zero results.

To avoid this:

  • Store all fixture bins used in let arithmetic as the same type (all INT or all FLOAT), or

  • Use .asFloat() / .asInt() explicitly to cast before combining.

-- All INT arithmetic:
let (total = $.price * $.qty, discount = ${total} / 10) then (${total} - ${discount} > 400)
-- Mixed types (NOTE: INT * FLOAT silently fails if types don't align):
let (total = $.price * $.qty, discount = ${total} * $.discount_rate) then (${total} - ${discount} > 400)
-- Fixed with explicit cast:
let (total = $.price * $.qty, discount = ${total}.asFloat() * $.discount_rate) then (${total}.asFloat() - ${discount} > 400.0)

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");

📖 API reference: DataSet.id(...) | Session.query(Key) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.execute() | RecordStream.getFirstRecord() | Record.getLong(...)

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()

📖 API reference: DataSet.id() | Session.query() | QueryBuilder.bin() | WriteSegmentBuilder.bin() | RecordStream.first_or_raise() | RecordStream.close() | QueryBuilder.execute()

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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.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:

MethodBehavior
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();

📖 API reference: DataSet.id(...) | Session.upsert(DataSet) | Session.upsert(Key) | ChainableOperationBuilder.bin(...) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.execute()

await (
session.upsert(users.id("u1"))
.bin("total").upsert_from("$.price * $.quantity")
.bin("discount").insert_from("$.coupon * 0.1")
.execute()
)

📖 API reference: DataSet.id() | Session.upsert() | QueryBuilder.bin() | WriteSegmentBuilder.bin() | WriteSegmentBuilder.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();

📖 API reference: DataSet.id(...) | Session.upsert(DataSet) | Session.upsert(Key) | ChainableOperationBuilder.bin(...) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.execute()

Where clause integration patterns

On dataset queries

session.query(users)
.where("$.age > 21")
.execute();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute()

stream = await session.query(users).where("$.age > 21").execute()

📖 API reference: Session.query() | QueryBuilder.where() | QueryBuilder.execute()

On single-key / batch operations

session.update(users.ids("u1", "u2"))
.bin("bonus").add(100)
.where("$.department == 'engineering'")
.execute();

📖 API reference: DataSet.ids(...) | Session.update(DataSet) | ChainableOperationBuilder.bin(...) | ChainableQueryBuilder.bin(...) | BinBuilder.add(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute()

await (
session.update(users.ids("u1", "u2"))
.bin("bonus").add(100)
.where("$.department == 'engineering'")
.execute()
)

📖 API reference: DataSet.ids() | Session.update() | QueryBuilder.where() | QueryBuilder.bin() | WriteSegmentBuilder.bin() | WriteSegmentBuilder.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();

📖 API reference: DataSet.id(...) | ChainableQueryBuilder.bin(...) | BinBuilder.add(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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()
)

📖 API reference: DataSet.id() | Session.update() | QueryBuilder.where() | QueryBuilder.bin() | WriteSegmentBuilder.bin() | WriteSegmentBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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()

📖 API reference: Session.query() | QueryBuilder.where() | RecordStream.close() | QueryBuilder.execute()

Feedback

Was this page helpful?

What type of feedback are you giving?

What would you like us to know?

+Capture screenshot

Can we reach out to you?