Skip to main content
Loading

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']]

Note that 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}}}