Computed Values

Introduced in: v3.10.0

You can configure collections to generate document attributes when documents are created or modified, using an AQL expression

If you want to add default values to new documents, maintain auxiliary attributes for search queries, or similar, you can set these attributes manually in every operation that inserts or modifies documents. However, it is more convenient and reliable to let the database system take care of it.

The computed values feature lets you define expressions on the collection level that run on inserts, modifications, or both. You can access the data of the current document to compute new values using a subset of AQL. The result of each expression becomes the value of a top-level attribute. These attributes are stored like any other attribute, which means that you may use them in indexes, but also that the schema validation is applied if you use one.

Possible use cases are to add timestamps of the creation or last modification to every document, to add default attributes, or to automatically process attributes for indexing purposes. For example, you can combine multiple attributes into one, filter out array elements, and convert text to lowercase characters, to then index the new attribute and use it to perform case-insensitive searches.

Using Computed Values

Computed values are defined per collection using the computedValues property, either when creating the collection or by modifying the collection later on. If you add or modify computed value definitions at a later point, then they only affect subsequent write operations. Existing documents remain in their state.

Computed value definitions are included in dumps, and the attributes they added, too, but no expressions are executed when restoring dumps. The collections and documents are restored as they are in the dump and no attributes are recalculated.

JavaScript API

The computedValues collection property accepts an array of objects.

db._create(<collection-name>, { computedValues: [ { … }, … ] })

db.<collection-name>.properties({ computedValues: [ { … }, … ] })

Each object represents a computed value and can have the following attributes:

  • name (string, required): The name of the target attribute. Can only be a top-level attribute, but you may return a nested object. Cannot be _key, _id, _rev, _from, _to, or a shard key attribute.

  • expression (string, required): An AQL RETURN operation with an expression that computes the desired value. See Computed Value Expressions for details.

  • overwrite (boolean, required): Whether the computed value shall take precedence over a user-provided or existing attribute.

  • computeOn (array, optional): An array of strings to define on which write operations the value shall be computed. The possible values are "insert", "update", and "replace". The default is ["insert", "update", "replace"].

  • keepNull (boolean, optional): Whether the target attribute shall be set if the expression evaluates to null. You can set the option to false to not set (or unset) the target attribute if the expression returns null. The default is true.

  • failOnWarning (boolean, optional): Whether to let the write operation fail if the expression produces a warning. The default is false.

HTTP API

See the computedValues collection property in the HTTP API documentation:

Computed Value Expressions

You can use a subset of AQL for computed values, namely a single RETURN operation with an expression that computes the value.

You can access the document data via the @doc bind variable. It contains the data as it will be stored, including the _key, _id, and _rev system attributes. On inserts, you get the user-provided values (plus the system attributes), and on modifications, you get the updated or replaced document to work with, including the user-provided values.

Computed value expressions have the following properties:

  • The expression must start with a RETURN operation and cannot contain any other operations. No FOR loops, LET statements, and subqueries are allowed in the expression. FOR loops can be substituted using the array expansion operator [*], for example, with an inline expressions like the following:

    RETURN @doc.values[* FILTER CURRENT > 42 RETURN CURRENT * 2]

  • You cannot access any stored data other than the current document via the @doc bind parameter. AQL functions that read from the database system cannot be used in the expression (e.g. DOCUMENT(), PREGEL_RESULT(), COLLECTION_COUNT()).

  • You cannot access the result of another computed value that is generated on the same computeOn event.

    For example, two computed values that are generated on insert cannot see the result of the other. Referencing the attributes results in an implicit null value. Computed values that are generated on update or replace can see the results of the previous insert computations, however. They cannot see the new values of other update and replace computations, regardless of the order of the computed value definitions in the computedValues property.

  • You can use AQL functions in the expression but only those that can be executed on DB-Servers, regardless of your deployment type. The following functions cannot be used in the expression:

    • CALL()
    • APPLY()
    • DOCUMENT()
    • V8()
    • SCHEMA_GET()
    • SCHEMA_VALIDATE()
    • VERSION()
    • COLLECTIONS()
    • CURRENT_USER()
    • CURRENT_DATABASE()
    • COLLECTION_COUNT()
    • NEAR()
    • WITHIN()
    • WITHIN_RECTANGLE()
    • FULLTEXT()
    • User-defined functions (UDFs)

Expressions that do not meet the requirements or that are syntactically invalid are rejected immediately, when setting or modifying the computed value definitions of a collection.

Examples

Add an attribute with the creation timestamp to new documents:

arangosh> var coll = db._create("users", {
........>   computedValues: [
........>     {
........>       name: "createdAt",
........>       expression: "RETURN DATE_NOW()",
........>       overwrite: true,
........>       computeOn: ["insert"]
........>     }
........>   ]
........> });
arangosh> var doc = db.users.save({ name: "Paula Plant" });
arangosh> db.users.toArray();
Show execution results
Hide execution results
[ 
  { 
    "_key" : "74086", 
    "_id" : "users/74086", 
    "_rev" : "_fyvNHtO---", 
    "name" : "Paula Plant", 
    "createdAt" : 1680612468468 
  } 
]

Add an attribute with the date and time of the last modification, only taking update and replace operations into (not inserts), and allowing to manually set this value instead of using the computed value:

arangosh> var coll = db._create("users", {
........>   computedValues: [
........>     {
........>       name: "modifiedAt",
........>       expression: "RETURN ZIP(['date', 'time'], SPLIT(DATE_ISO8601(DATE_NOW()), 'T'))",
........>       overwrite: false,
........>       computeOn: ["update", "replace"]
........>     }
........>   ]
........> });
arangosh> var doc = db.users.save({ _key: "123", name: "Paula Plant" });
arangosh> doc = db.users.update("123", { email: "gardener@arangodb.com" });
arangosh> db.users.toArray();
arangosh> doc = db.users.update("123", { email: "greenhouse@arangodb.com", modifiedAt: { date: "2019-01-01", time: "20:30:00.000Z" } });
arangosh> db.users.toArray();
Show execution results
Hide execution results
{ 
  "_id" : "users/123", 
  "_key" : "123", 
  "_rev" : "_fyvNHty---", 
  "_oldRev" : "_fyvNHtu--_" 
}
[ 
  { 
    "_key" : "123", 
    "_id" : "users/123", 
    "_rev" : "_fyvNHty---", 
    "name" : "Paula Plant", 
    "email" : "gardener@arangodb.com", 
    "modifiedAt" : { 
      "date" : "2023-04-04", 
      "time" : "12:47:48.477Z" 
    } 
  } 
]
{ 
  "_id" : "users/123", 
  "_key" : "123", 
  "_rev" : "_fyvNHt2---", 
  "_oldRev" : "_fyvNHty---" 
}
[ 
  { 
    "_key" : "123", 
    "_id" : "users/123", 
    "_rev" : "_fyvNHt2---", 
    "name" : "Paula Plant", 
    "email" : "greenhouse@arangodb.com", 
    "modifiedAt" : { 
      "date" : "2019-01-01", 
      "time" : "20:30:00.000Z" 
    } 
  } 
]

Compute an attribute from two arrays, filtering one of the lists, and calculating new values to implement a case-insensitive search using a persistent array index:

arangosh> var coll = db._create("users", {
........>   computedValues: [
........>     {
........>       name: "searchTags",
........>       expression: "RETURN APPEND(@doc.is[* FILTER CURRENT.public == true RETURN LOWER(CURRENT.name)], @doc.loves[* RETURN LOWER(CURRENT)])",
........>       overwrite: true
........>     }
........>   ]
........> });
arangosh> var doc = db.users.save({ name: "Paula Plant", is: [ { name: "Gardener", public: true }, { name: "female" } ], loves: ["AVOCADOS", "Databases"] });
arangosh> var idx = db.users.ensureIndex({ type: "persistent", fields: ["searchTags[*]"] });
arangosh> db._query(`FOR u IN users FILTER "avocados" IN u.searchTags RETURN u`).toArray();
Show execution results
Hide execution results
[ 
  { 
    "_key" : "74059", 
    "_id" : "users/74059", 
    "_rev" : "_fyvNHs2---", 
    "name" : "Paula Plant", 
    "is" : [ 
      { 
        "name" : "Gardener", 
        "public" : true 
      }, 
      { 
        "name" : "female" 
      } 
    ], 
    "loves" : [ 
      "AVOCADOS", 
      "Databases" 
    ], 
    "searchTags" : [ 
      "gardener", 
      "avocados", 
      "databases" 
    ] 
  } 
]

Set keepNull to false and let an expression return null to not set or unset the target attribute. If you set overwrite to false at the same time, then the target attribute is not actively unset:

arangosh> var coll = db._create("users", {
........>   computedValues: [
........>     {
........>       name: "fullName",
........>       expression: "RETURN @doc.firstName != null AND @doc.lastName != null ? CONCAT_SEPARATOR(' ', @doc.firstName, @doc.lastName) : null",
........>       overwrite: false,
........>       keepNull: false
........>     }
........>   ]
........> });
arangosh> var docs = db.users.save([
........>   { firstName: "Paula", lastName: "Plant" },
........>   { firstName: "James" },
........>   { lastName: "Barrett", fullName: "Andy J. Barrett" }
........> ]);
arangosh> db.users.toArray();
Show execution results
Hide execution results
[ 
  { 
    "_key" : "74110", 
    "_id" : "users/74110", 
    "_rev" : "_fyvNHte---", 
    "firstName" : "Paula", 
    "lastName" : "Plant", 
    "fullName" : "Paula Plant" 
  }, 
  { 
    "_key" : "74111", 
    "_id" : "users/74111", 
    "_rev" : "_fyvNHte--_", 
    "firstName" : "James" 
  }, 
  { 
    "_key" : "74112", 
    "_id" : "users/74112", 
    "_rev" : "_fyvNHte--A", 
    "lastName" : "Barrett", 
    "fullName" : "Andy J. Barrett" 
  } 
]

Add a computed value as a sub-attribute to documents. This is not possible directly because the target attribute needs to be a top-level attribute, but the AQL expression can merge a nested object with the top-level attribute to achieve this. The expression checks whether the attributes it wants to calculate a new value from exist and are strings. If the preconditions are not met, then it returns the original name attribute:

arangosh> var coll = db._create("users", {
........>   computedValues: [
........>     {
........>       name: "name",
........>       expression: "RETURN IS_STRING(@doc.name.first) AND IS_STRING(@doc.name.last) ? MERGE(@doc.name, { 'full': CONCAT_SEPARATOR(' ', @doc.name.first, @doc.name.last) }) : @doc.name",
........>       overwrite: true // must be true to replace the top-level "name" attribute
........>     }
........>   ]
........> });
arangosh> var docs = db.users.save([
........>   { name: { first: "James" } },
........>   { name: { first: "Paula", last: "Plant" } }
........> ]);
arangosh> db.users.toArray();
Show execution results
Hide execution results
[ 
  { 
    "_key" : "74164", 
    "_id" : "users/74164", 
    "_rev" : "_fyvNHuK---", 
    "name" : { 
      "first" : "James" 
    } 
  }, 
  { 
    "_key" : "74165", 
    "_id" : "users/74165", 
    "_rev" : "_fyvNHuK--_", 
    "name" : { 
      "first" : "Paula", 
      "last" : "Plant", 
      "full" : "Paula Plant" 
    } 
  } 
]