CDT Operations¶
Complex Data Type (CDT) operations let you read and modify nested lists and maps within a single bin, server-side, without fetching the whole record.
Reading CDT Data¶
Navigate into a bin’s structure using on_list() and on_map_key():
users = DataSet.of("test", "users")
# Read a map value by key
stream = await (
session.query(users.id(1))
.bin("settings").on_map_key("theme").get()
.execute()
)
# Read a list element by index
stream = await (
session.query(users.id(1))
.bin("scores").on_list_index(0).get()
.execute()
)
# Read a nested value: $.profile.address.city
stream = await (
session.query(users.id(1))
.bin("profile").on_map_key("address").on_map_key("city").get()
.execute()
)
List Ranges¶
# Get items by index range
stream = await (
session.query(users.id(1))
.bin("scores").on_list_index_range(0, 3).get()
.execute()
)
# Get items by value range
stream = await (
session.query(users.id(1))
.bin("scores").on_list_value_range(10, 100).get()
.execute()
)
Map Ranges¶
# Get map entries by key range
stream = await (
session.query(users.id(1))
.bin("metrics").on_map_key_range("a", "m").get()
.execute()
)
# Get by rank (top N values)
stream = await (
session.query(users.id(1))
.bin("scores").on_map_value_rank_range(-3).get()
.execute()
)
Collection Metadata¶
# Get list size
stream = await (
session.query(users.id(1))
.bin("scores").on_list().size()
.execute()
)
# Check if key exists in map
stream = await (
session.query(users.id(1))
.bin("settings").on_map_key("theme").exists()
.execute()
)
Joining String Lists¶
list_join concatenates the string items of a list into a single string.
The separator is optional; the list must hold only strings, and an empty
list joins to an empty string.
# ["one", "two", "three"] → "one,two,three"
stream = await (
session.query(users.id(1))
.bin("tags").list_join(",")
.execute()
)
# Join a list nested under a map key
stream = await (
session.query(users.id(1))
.bin("profile").on_map_key("nicknames").list_join(", ")
.execute()
)
String Operations on a Nested Leaf¶
When a navigated path lands on a string, the str_* family is available on
the navigation builder itself: reads such as str_strlen and str_contains
on either verb, modifies such as str_append and str_upper on write verbs.
See the nested-strings section of String Operations.
# Uppercase one nickname in place
await (
session.upsert(users.id(1))
.bin("profile").on_map_key("nicknames").on_list_index(0).str_upper()
.execute()
)
Writing CDT Data¶
Set a Value¶
# Set a map key
await (
session.update(users.id(1))
.bin("settings").on_map_key("theme").set_to("dark")
.execute()
)
# Set a list element by index
await (
session.update(users.id(1))
.bin("scores").on_list_index(0).set_to(99)
.execute()
)
Add / Increment¶
# Increment a map value
await (
session.update(users.id(1))
.bin("counters").on_map_key("views").add(1)
.execute()
)
List Operations¶
# Append to a list
await (
session.update(users.id(1))
.bin("scores").list_append(95)
.execute()
)
# Add item (insert-sorted for ordered lists)
await (
session.update(users.id(1))
.bin("scores").list_add(95)
.execute()
)
# Append multiple items
await (
session.update(users.id(1))
.bin("tags").list_append_items(["python", "aerospike"])
.execute()
)
# The same list writes work on a list reached by navigation
await (
session.update(users.id(1))
.bin("teams").on_map_key("team1").list_append("Diana")
.execute()
)
# Clear a list
await (
session.update(users.id(1))
.bin("scores").list_clear()
.execute()
)
# Sort a list
await (
session.update(users.id(1))
.bin("scores").list_sort()
.execute()
)
Map Operations¶
# Upsert map entries
await (
session.update(users.id(1))
.bin("settings").map_upsert_items({"theme": "dark", "lang": "en"})
.execute()
)
# Clear a map
await (
session.update(users.id(1))
.bin("settings").map_clear()
.execute()
)
Key-ordered maps¶
A map written as a plain dict is stored unordered. The server sorts the
entries either way, so a read looks identical — but it will not binary-search a
map that was not declared ordered, and keyed or range access on one falls back
to a scan. On a large map that is the difference between a lookup and a walk.
Declare the order by wrapping the dict in SortedMap:
from aerospike_sdk import SortedMap
await (
session.upsert(users.id(1))
.put({"scores": SortedMap({"zoe": 3, "amy": 1})})
.execute()
)
The flag is stored with the record and survives later modification, so it governs the cost of every subsequent access — by any client — until the map is rewritten unordered.
SortedMap subclasses dict, so it behaves as one everywhere, and a
key-ordered map reads back as a SortedMap rather than a plain dict:
scores = record.bins["scores"]
scores["amy"] # 1
scores == {"amy": 1, "zoe": 3} # True
isinstance(scores, dict) # True
A selection read with get_keys_and_values() is a plain dict in the order
the server selected the entries, such as rank order for a rank range.
get_as_map() is the same dict with no order promise. End the selection
with get_as_ordered_map() instead to get a SortedMap in key order, whatever
the bin’s own order:
stream = await (
session.query(users.id(1))
.bin("scores").on_map_rank_range(-3, 3).get_as_ordered_map()
.execute()
)
top_three = (await stream.first_or_raise()).record.bins["scores"] # SortedMap
Maps created through the CDT surface take their order from the operation
instead, so SortedMap is not needed there:
await (
session.update(users.id(1))
.bin("settings").map_upsert_items({"theme": "dark"}, order=MapOrder.KEY_ORDERED)
.execute()
)
Remove¶
# Remove a map key
await (
session.update(users.id(1))
.bin("settings").on_map_key("deprecated_key").remove()
.execute()
)
# Remove a list element
await (
session.update(users.id(1))
.bin("scores").on_list_index(-1).remove()
.execute()
)
Paths: acting on every child at once¶
The navigation above picks out one element. A path selects a whole level — every child, or only those matching a predicate — and applies a single server operation across the selection.
on_each_child() walks every element; on_each_child_where(pred) keeps the
matches. The predicate is an Exp over the current element’s loop variable:
from aerospike_sdk import Exp, LoopVarPart
# add 10 to every element of the list
add_10 = Exp.num_add([Exp.int_loop_var(LoopVarPart.VALUE), Exp.val(10)])
await session.update(key).bin("nums").on_each_child().modify_by(add_10).execute()
# drop just the elements over 5
over_5 = Exp.gt(Exp.int_loop_var(LoopVarPart.VALUE), Exp.val(5))
await (
session.update(key).bin("nums").on_each_child_where(over_5).remove_matches().execute()
)
Reads use the collect_* terminals — named apart from get_* because they
return a selection rather than one element:
Terminal |
Returns |
|---|---|
|
the value of every selected element |
|
the key of every selected map entry |
|
selected map entries as key/value pairs |
|
the selection, still nested as it was found |
Paths nest, so a predicate can apply a level down:
# every value over 5, in every list under the map
result = await (
await session.query(key)
.bin("m").on_each_child().on_each_child_where(over_5).collect_values()
.execute()
).first_or_raise()
Selecting map entries by key¶
on_map_keys_in(keys) selects the entries of a map whose key is in keys; keys
the map does not hold are skipped. It opens a path from a bin, or continues one
after any navigation step. and_filter(pred) then narrows that selection with a
predicate over each entry:
# the values under "alpha" and "gamma"
result = await (
await session.query(key)
.bin("m").on_map_keys_in(["alpha", "gamma"]).collect_values()
.execute()
).first_or_raise()
# entries a, b, c whose value is over 10, as key/value pairs
over_10 = Exp.gt(Exp.int_loop_var(LoopVarPart.VALUE), Exp.val(10))
result = await (
await session.query(key)
.bin("m").on_map_keys_in(["a", "b", "c"]).and_filter(over_10).collect_map_entries()
.execute()
).first_or_raise()
# add 10 to just the selected entries
await session.update(key).bin("m").on_map_keys_in(["a", "c"]).modify_by(add_10).execute()
keys is any collection, and keys of different types may be mixed. A bytes
element is one blob key. A bare str or bytes in place of the collection
raises TypeError rather than selecting one key per character or byte; wrap a
single key in a list: on_map_keys_in([b"\x01\x02"]). The same holds for
on_map_key_list, on_map_value_list and on_list_value_list.
and_filter refines the step before it, so it follows on_map_keys_in or a
single-element step such as on_map_key, once per level. After a
single-element step it keeps that element only if it matches, and opens a path
there:
# the "book" price, only if it is over 10
.bin("prices").on_map_key("book").and_filter(over_10).collect_values()
The builder raises TypeError where the server would reject it: as the first
step, after on_each_child / on_each_child_where (put the predicate there
instead), or after another and_filter (combine conditions with Exp.and_ in
one call). It also refuses a range or multi-element selection, which has no
single element to filter, and on_map_index / on_map_rank, where the server
does not return the filtered value correctly.
Writes have modify_by(expr), modify_no_fail(expr) — which tolerates elements
the expression cannot be applied to — and remove_matches(). For explicit
SelectFlags, collect_by_path(flags) is the unsugared form.
An empty or absent collection is never an error; the read yields an empty list.
A path filter that meets an element it cannot evaluate, such as an integer
comparison against a string, fails the operation unless you pass no_fail=True,
which skips that element instead:
# "mixed" holds ["a", 7, 9]: without no_fail the string fails the read
.bin("d").on_map_key("mixed").on_each_child_where(over_5).collect_values(no_fail=True)
# -> [7, 9]
collect_values_as_expression_read(bin_type) reads the same selection through
an expression read instead of a CDT operation. Pass the bin’s type,
ExpType.MAP or ExpType.LIST; the result is a list under the bin’s name. It
takes no_fail as well, and ignore_eval_failure=True leaves the bin out of
the result, rather than failing, when the evaluation itself fails, for example
because the bin is not of the declared type:
from aerospike_sdk import ExpType
result = await (
await session.query(key)
.bin("catalog").on_map_key("book").on_each_child().on_map_key("title")
.collect_values_as_expression_read(ExpType.MAP)
.execute()
).first_or_raise()
titles = result.record.bins["catalog"]
For select flags other than values, build the expression with
Exp.exp_select_by_path and pass it to select_from.
AEL expressions on CDT¶
AEL supports CDT paths for filtering. A collection predicate like the ones
below is generally not satisfiable from a secondary index, so it falls back to
a primary-index (full-set) scan. That is allowed by default; set
allow_scans_with_where=False on the query or the Behavior to reject the
fallback instead:
# Filter records where the list has more than 5 items
stream = await (
session.query(users)
.where("$.scores:LIST.count() > 5")
.execute()
)
# Filter on a nested map value
stream = await (
session.query(users)
.where('$.settings.theme == "dark"')
.execute()
)
A collection index can serve some of these predicates directly; see Secondary Indexes.