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

```java
session.query(users)

    .where("$.age > 21 and $.status == 'active'")

    .execute();
```

```python
stream = await session.query(users).where("$.age > 21 and $.status == 'active'").execute()

# stream.close() is synchronous (not await)
```

Single-key and batch operations:

```java
session.update(users.id("u1"))

    .bin("lastSeen").setTo(System.currentTimeMillis())

    .where("$.status == 'active'")

    .execute();
```

```python
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.

```java
PreparedAel prepared = new PreparedAel("$.age > $1 and $.name == $2");

session.query(users)

    .where(prepared, 21, "Tim")

    .execute();
```

```python
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 `$`:

```plaintext
$.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:

| Path | Inferred type |
| --- | --- |
| `$.x.name` | Map (first context is identifier) |
| `$.x.[0]` | List (first context is `[`) |
| `$.x > 5` | Scalar (no context) |

### Comparison operators

```plaintext
$.age > 21

$.name == 'Tim'

$.price >= 100.0

$.status != 'inactive'
```

Both sides can be bins (requires explicit type):

```plaintext
$.binA.get(type: INT) > $.binB.get(type: INT)
```

### The `in` operator

```plaintext
$.name in ["Bob", "Mary", "Richard"]

"gold" in $.allowedStatuses
```

### Logical operators

```plaintext
$.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:

```plaintext
$.a > 1 and ($.b > 2 or $.c > 3)
```

### Arithmetic

```plaintext
($.price * $.qty) > 1000

($.apples + 5) > 10

abs($.score - 50) < 10

max($.a, $.b, $.c) > 100
```

Both operands must be the same type. Use `asInt()` / `asFloat()` to convert:

```plaintext
$.intBin + $.floatBin.asInt()
```

### Record metadata

```plaintext
$.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

```plaintext
$.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 rank
```

### List access

```plaintext
$.scores.[0]                    -- by index

$.scores.[-1]                   -- last element

$.scores.[=42]                  -- by value

$.scores.[#0]                   -- by rank (lowest)
```

### Plural selectors (leaf only)

```plaintext
$.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 range
```

### Inverted selections (prefix `!`)

```plaintext
$.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

```plaintext
$.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

```plaintext
$.listBin.[].count()             -- list size

$.mapBin.{}.count()              -- map size

$.listBin.[=4].count() > 0      -- count of elements equal to 4
```

### `exists()` — existence check

```plaintext
$.binA.exists() and $.binB.exists()

$.mapBin.address.exists()
```

## Control structures

### Conditional: `when`

```plaintext
when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze')
```

`default` clause is mandatory.

### Variable binding: `let ... then`

```plaintext
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.

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

```python
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:

```java
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 |

```java
session.upsert(users.id("u1"))

    .bin("total").upsertFrom("$.price * $.quantity")

    .bin("discount").insertFrom("$.coupon * 0.1")

    .execute();
```

```python
await (

    session.upsert(users.id("u1"))

    .bin("total").upsert_from("$.price * $.quantity")

    .bin("discount").insert_from("$.coupon * 0.1")

    .execute()

)
```

Options for write expressions:

```java
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

```java
session.query(users)

    .where("$.age > 21")

    .execute();
```

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

### On single-key / batch operations

```java
session.update(users.ids("u1", "u2"))

    .bin("bonus").add(100)

    .where("$.department == 'engineering'")

    .execute();
```

```python
await (

    session.update(users.ids("u1", "u2"))

    .bin("bonus").add(100)

    .where("$.department == 'engineering'")

    .execute()

)
```

### On mixed batch chains (per-operation and default)

```java
session

    .update(users.id("u1")).bin("bonus").add(100)

        .where("$.department == 'engineering'")

    .update(users.id("u2")).bin("bonus").add(50)

    .defaultWhere("$.active == true")

    .execute();
```

```python
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

```java
PreparedAel activeInDept = new PreparedAel("$.active == true and $.department == $1");

session.query(users)

    .where(activeInDept, "engineering")

    .execute();

session.query(users)

    .where(activeInDept, "marketing")

    .execute();
```

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