Context for Operations on Nested Elements
Collection data type (CDT) contexts allow operations to be targeted at elements that are nested within a List or Map.
- Elements at the top level of the collection do not need a context to be accessed.
- The context identifies the path to the element you wish to operate on.
- You can also create elements using a context when the path being described isn't fully there.
- The CDT context feature was added in Aerospike Database version 4.6.
CDT Context APIโ
The following describes the CDT Context API in generic terms. Each language
client might have slightly different terms to express the same concepts, such as
the Java client's CTX
class.
Most operations in the List and Map API take an optional context parameter.
The Context is a list of element selector pairs targeting a specific element, which is nested in a List or Map. Each element selector includes a context type and a value.
Context Typeโ
This specifies the type of element selector, which will be applied from the top level of the collection, or the point arrived at by the previous step in the Context.
BY_LIST_INDEX(index)
BY_LIST_RANK(rank)
BY_LIST_VALUE(value)
BY_MAP_INDEX(index)
BY_MAP_RANK(rank)
BY_MAP_KEY(key)
BY_MAP_VALUE(value)
Two new Context types were added in Aerospike version 4.9, which can create an
element if it doesn't exist, then select it. This is similar to how
mkdir -p /creates/the/path/to/a/sub/directory
.
MAP_KEY_CREATE(key)
LIST_INDEX_CREATE(index)
Language-Specific Client APIsโ
Java | C | C# | Go | Python | Node.js | PHP | Ruby | REST | Rust
Context Example - Drilling Downโ
Consider the following list:
[0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
We can operate on the List element [3, 4]
by identifying a context for the
operation using
[BY_LIST_INDEX(2), BY_LIST_INDEX(1)]
- The first context type
BY_LIST_INDEX(2)
selects the third element of the top level List. The element selected by it is the List[2, [3, 4], 5, 6]]
. - The second context type
BY_LIST_INDEX(1)
selects the element at index position 1. The element selected by it is the List[3, 4]
. - A List API operation can now be applied to one of the elements within this nested List.
Operation Examplesโ
Examples with List append()โ
In each example we will start with a bin 'l', which contains a List value
[0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
This List can be visualized as
[0, 1, [ ], 7, [ ]] # top level (depth 0)
2, [ ], 5, 6 8, 9 # depth 1 nested elements
3, 4 # depth 2 nested elements
At the top level we have five elements, three of them scalar Integer values, two of them are List values.
The List value at index position 2 has four elements - the Integer value 2, a List element, then the Integer values 5 and 6. Its List element at index position 1 has two elements, the Integers 3 and 4.
The List append()
operation adds a
single item to the end of a List. We'll be using it in the next few examples.
Append an item to a List at the last elementโ
Append the value 100 to the List nested at the last element of the top level.
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(-1)])
Result:
[0, 1, [2, [3, 4, 100], 5, 6], 7, [8, 9, 100]]
Without the context we are appending to the top level List.
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100)
Result:
[0, 1, [2, [3, 4], 5, 6], 7, [8, 9], 100]
Append an item to a non-List elementโ
Let's append the value 100 to a List element at index position 0. Hint: there's an integer value at index 0.
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(0)])
# Error 26 and no change [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
Error:
Error code 26 OP_NOT_APPLICABLE
This is because a List context type needs to identify a List element, and a Map context type needs to identify a Map element.
Append an item to depth 2โ
Let's append the value 100 to the deepest List, which is at depth 2. This means that we'll need a Context with two context types to select our way to that List.
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(2), BY_LIST_INDEX(1)])
Result:
[0, 1, [2, [3, 4, 100], 5, 6], 7, [8, 9]]
Examples for Selecting a List Element by Valueโ
In the next examples we will start with a bin 'l', which contains a List of tuples, each expressed as a List element
[[1, 'a'], [2, 'b'], [4, 'd'], [2, 'bb']]
This List can be visualized as
[[ ], [ ], [ ], [ ]] # top level (depth 0)
1, 'a' 2, 'b' 4, 'd' 2, 'bb' # depth 1 nested elements
Append a Map to the first tuple starting with 2โ
We will select an element by value using
a wildcard. Where the
List get_all_by_value
returns every match, the context type BY_LIST_VALUE
will return the first,
depending on the ascending value order of the elements in the List.
# [[1, 'a'], [2, 'b'], [4, 'd'], [2, 'bb']]
list_append('l', {3: {'c': '๐'}}, context=[BY_LIST_VALUE([2, *])])
Result:
[[1, 'a'], [2, 'b', {3: {'c': '๐'}}], [4, 'd'], [2, 'bb']]
This would not be valid JSON, but it is a valid Aerospike collection. Aerospike Maps accept Integer map keys (as well as String, binary data (Bytes) and Double).
When executing 'by value' operations on a List, Aerospike uses the value order.
If this is an Unordered List, this value order is computed just-in-time. In this
case, the value order of the tuples ahead of the List append()
operation was
[[1, 'a'], [2, 'b'], [2, 'bb'], [4, 'd']]
Disambiguate a 'by List value' selectionโ
In the previous example we saw how selection by List value may be ambiguous. In
the following example we get more particular. We will read the value for map key
'c' from the nested Map by giving the Map get_by_key()
operation a Context that selects a List element by value, then gets the
inner Map by map key 3.
# [[1, 'a'], [2, 'b', {3: {'c': '๐'}}], [4, 'd'], [2, 'bb']]
map_get_by_key('l', 'c', context=[BY_LIST_VALUE([2, 'b', *]), BY_MAP_KEY(3)])
Result:
'๐'
Creating a Contextโ
Sometimes the context you want to perform an operation on does not exist. Before
Aerospike version 4.9 you needed to create every element along the way first,
typically with a CREATE_ONLY | NO_FAIL
combination of write flags. Starting
with version 4.9 the new MAP_KEY_CREATE
context type simplifies this process.
In the following example we want to add accolades to the stats of an actor, and accolades is a Map that can have arbitrary data. The data is in a bin m
{'name': 'chuck norris'}
What if we want to Map increment
a jokes accolade by 317, and neither 'accolades' nor 'jokes' exists yet?
What if we're not sure if this path exists, and want the operation to always succeed?
map_increment('m', 'jokes', 317, context=[MAP_KEY_CREATE('stats'), MAP_KEY_CREATE('accolades')])
Result
{'name': 'chuck norris', 'stats': {'accolades': {'jokes': 317}}}
Before Aerospike version 4.9 you needed to create a multi-operation transaction
by leveraging Map put()
to create the elements of the context ahead of the increment operation.
ops = [
map_put('m', 'stats', {}, CREATE_ONLY|NO_FAIL),
map_put('m', 'accolades', {}, CREATE_ONLY|NO_FAIL),
map_increment('m', 'jokes', 317,
context=[MAP_KEY_CREATE('stats'), MAP_KEY_CREATE('accolades')]),
]
Result
{'name': 'chuck norris', 'stats': {'accolades': {'jokes': 317}}}