Using dynamic attribute names in AQL
Problem
I want an AQL query to return results with attribute names assembled by a function, or with a variable number of attributes.
This will not work by specifying the result using a regular object literal, as object literals require the names and numbers of attributes to be fixed at query compile time.
Solution
There are several solutions to getting dynamic attribute names to work.
Subquery solution
A general solution is to let a subquery or another function to produce the dynamic
attribute names, and finally pass them through the ZIP()
function to create an object
from them.
Let's assume we want to process the following input documents:
{ "name" : "test", "gender" : "f", "status" : "active", "type" : "user" }
{ "name" : "dummy", "gender" : "m", "status" : "inactive", "type" : "unknown", "magicFlag" : 23 }
Let's also assume our goal for each of these documents is to return only the attribute
names that contain the letter a
, together with their respective values.
To extract the attribute names and values from the original documents, we can use a subquery as follows:
LET documents = [
{ "name" : "test"," gender" : "f", "status" : "active", "type" : "user" },
{ "name" : "dummy", "gender" : "m", "status" : "inactive", "type" : "unknown", "magicFlag" : 23 }
]
FOR doc IN documents
RETURN (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: name,
value: doc[name]
}
)
The subquery will only let attribute names pass that contain the letter a
. The results
of the subquery are then made available to the main query and will be returned. But the
attribute names in the result are still name
and value
, so we're not there yet.
So let's also employ AQL's ZIP()
function, which can create an object from two arrays:
- the first parameter to
ZIP()
is an array with the attribute names - the second parameter to
ZIP()
is an array with the attribute values
Instead of directly returning the subquery result, we first capture it in a variable, and
pass the variable's name
and value
components into ZIP()
like this:
LET documents = [
{ "name" : "test"," gender" : "f", "status" : "active", "type" : "user" },
{ "name" : "dummy", "gender" : "m", "status" : "inactive", "type" : "unknown", "magicFlag" : 23 }
]
FOR doc IN documents
LET attributes = (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: name,
value: doc[name]
}
)
RETURN ZIP(attributes[*].name, attributes[*].value)
Note that we have to use the expansion operator ([*]
) on attributes
because attributes
itself is an array, and we want either the name
attribute or the value
attribute of each
of its members.
To prove this is working, here is the above query's result:
[
{
"name": "test",
"status": "active"
},
{
"name": "dummy",
"status": "inactive",
"magicFlag": 23
}
]
As can be seen, the two results have a different amount of result attributes. We can also
make the result a bit more dynamic by prefixing each attribute with the value of the name
attribute:
LET documents = [
{ "name" : "test"," gender" : "f", "status" : "active", "type" : "user" },
{ "name" : "dummy", "gender" : "m", "status" : "inactive", "type" : "unknown", "magicFlag" : 23 }
]
FOR doc IN documents
LET attributes = (
FOR name IN ATTRIBUTES(doc)
FILTER LIKE(name, '%a%')
RETURN {
name: CONCAT(doc.name, '-', name),
value: doc[name]
}
)
RETURN ZIP(attributes[*].name, attributes[*].value)
That will give us document-specific attribute names like this:
[
{
"test-name": "test",
"test-status": "active"
},
{
"dummy-name": "dummy",
"dummy-status": "inactive",
"dummy-magicFlag": 23
}
]
Using expressions as attribute names (ArangoDB 2.5)
If the number of dynamic attributes to return is known in advance, and only the attribute names need to be calculated using an expression, then there is another solution.
ArangoDB 2.5 and higher allow using expressions instead of fixed attribute names in object literals.
Using expressions as attribute names requires enclosing the expression in extra [
and ]
to
disambiguate them from regular, unquoted attribute names.
Let's create a result that returns the original document data contained in a dynamically named
attribute. We'll be using the expression doc.type
for the attribute name. We'll also return
some other attributes from the original documents, but prefix them with the documents' _key
attribute values. For this we also need attribute name expressions.
Here is a query showing how to do this. The attribute name expressions all required to be
enclosed in [
and ]
in order to make this work:
LET documents = [
{ "_key" : "3231748397810", "gender" : "f", "status" : "active", "type" : "user" },
{ "_key" : "3231754427122", "gender" : "m", "status" : "inactive", "type" : "unknown" }
]
FOR doc IN documents
RETURN {
[ doc.type ] : {
[ CONCAT(doc._key, "_gender") ] : doc.gender,
[ CONCAT(doc._key, "_status") ] : doc.status
}
}
This will return:
[
{
"user": {
"3231748397810_gender": "f",
"3231748397810_status": "active"
}
},
{
"unknown": {
"3231754427122_gender": "m",
"3231754427122_status": "inactive"
}
}
]
Note: attribute name expressions and regular, unquoted attribute names can be mixed.
Author: Jan Steemann
Tags: #aql