User-defined functions

User-defined functions (UDFs) allow developers to combine Fauna Query Language functions, either built-in or user-defined, into named queries that can be executed repeatedly.

By default, UDFs run with the privileges of the current query session. For example, if your client code connects to Fauna using a key with the server role, any called UDFs run with, by default, server privileges. A UDF can, however, be assigned a specific role and execute with different privileges than the caller has.

UDFs are useful for a variety of purposes in an application, and this section demonstrates a few of them. Consider a sample collection called products with the following documents:

{
  "ref": Ref(Collection("products"), "321796915277595220"),
  "ts": 1643134348900000,
  "data": { "name": "avocados", "quantity": 100, "price": 3.99 }
},
{
  "ref": Ref(Collection("products"), "321796915277593728"),
  "ts": 1643134936720000,
  "data": { "name": "limes", "quantity": 30, "price": 0.35 }
},
{
  "ref": Ref(Collection("products"), "321796915277523910"),
  "ts": 1643134936720000,
  "data": { "name": "cilantro", "quantity": 100, "price": 1.49 }
}

Before you add a new item to this collection, you might want to perform some data validation to ensure consistent document structure. For the purposes of this demonstration, let’s assume that the name must exist within the collection, and the price must be a Double (a double-precision, floating-point number). With a UDF, you can create a function which performs the validation and only creates the new document if its fields contain valid values.

The C# version of this example is not currently available.
The Go version of this example is not currently available.
The Java version of this example is not currently available.
client.query(
  q.CreateFunction({
    name: 'create_new_item', (1)
    body:
      q.Query(
        q.Lambda( (2)
          // the function takes three parameters
          ['name', 'quantity', 'price'],
          q.Let( (3)
            {
              // define two variables
              is_name_known: q.If( (4)
                q.GT(q.Count(q.Match(q.Index('known_names'), q.Var('name'))), 0),
                true,
                false
              ),
              is_price_double: q.IsDouble(q.Var('price')),
            },
            // if checks pass, perform new document creation
            q.If(
              q.And(q.Var('is_name_known'), q.Var('is_price_double')),
              q.Create(q.Collection('products'), { (5)
                data: { name: q.Var('name'), quantity: q.Var('quantity'), price: q.Var('price') },
              }),
              // if checks don't pass, assemble error message
              q.Abort( (6)
                q.Concat(
                  [
                    q.If(q.Var('is_name_known'), '', 'Name is unknown. '),
                    q.If(q.Var('is_price_double'), '', 'Price is not a double. '),
                  ],
                  ''
                )
              )
            )
          )
        )
      ),
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s\n%s',
  err.name,
  err.message,
  err.errors()[0].description,
  err.errors()[0].cause[0],
))
{
  ref: Function("create_new_item"),
  ts: 1643153639080000,
  name: 'create_new_item',
  body: Query(Lambda(["name", "quantity", "price"], Let({"is_name_known": If(GT(Count(Match(Index("known_names"), Var("name"))), 0), true, false), "is_price_double": IsDouble(Var("price"))}, If(And(Var("is_name_known"), Var("is_price_double")), Create(Collection("products"), {"data": {"name": Var("name"), "quantity": Var("quantity"), "price": Var("price")}}), Abort(Concat([If(Var("is_name_known"), "", "Name is unknown. "), If(Var("is_price_double"), "", "Price is not a double. ")], ""))))))
}
The Python version of this example is not currently available.
The Shell version of this example is not currently available.
Query metrics:
  •    bytesIn:  731

  •   bytesOut:  831

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   28

  • writeBytes:  833

  •  queryTime: 14ms

  •    retries:    0

1 On line 3, we give the function a name: create_new_item.
2 The new function itself, defined starting on line 5, is one big Lambda, or anonymous function. This function takes three inputs: name, quantity, and price.
3 In order to test for our two conditions, we use Let to create two variables, each of which returns a boolean. The is_name_known variable checks the known_names index to see if the brand input exists, and the is_price_double variable checks to see if the price input is in fact a Double.
4 The If function behaves as a ternary operator: "If X then Y, otherwise Z". The If on line 12 checks to see if the number of results matching name in the known_names is index is greater than 0, then returns true if it is, or false if it isn’t.
5 The Create function is enclosed in an If function, so that if the checks don’t pass, the document is not created. Instead, we halt the query with Abort, which allows us to return an error message.
6 The contents of the error message depend on the contents of the two boolean variables, is_name_known and is_price_double.

Before we can use the new UDF we need to create the index known_names:

The C# version of this example is not currently available.
The Go version of this example is not currently available.
The Java version of this example is not currently available.
client.query(
  q.CreateIndex({
    name: 'known_names',
    source: q.Collection('products'),
    terms: [
      { field: ['data', 'name'] },
    ],
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s\n%s',
  err.name,
  err.message,
  err.errors()[0].description,
  err.errors()[0].cause[0],
))
{
  ref: Index("known_names"),
  ts: 1643149061020000,
  active: true,
  serialized: true,
  name: "known_names",
  source: Collection("products"),
  terms: [
    {
      field: ["data", "name"]
    }
  ],
  partitions: 1
}
The Python version of this example is not currently available.
The Shell version of this example is not currently available.
Query metrics:
  •    bytesIn:   132

  •   bytesOut:   294

  • computeOps:     1

  •    readOps:     0

  •   writeOps:    10

  •  readBytes: 3,659

  • writeBytes: 1,580

  •  queryTime:  57ms

  •    retries:     0

To run the UDF, we use the Call function:

The C# version of this example is not currently available.
The Go version of this example is not currently available.
The Java version of this example is not currently available.
client.query(
  q.Call(q.Function('create_new_item'), ['avocados', 100, 2.89])
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s\n%s',
  err.name,
  err.message,
  err.errors()[0].description,
  err.errors()[0].cause[0],
))
{
  ref: Ref(Collection("products"), "321796915277595220"),
  ts: 1643148303270000,
  data: {
    name: "avocados",
    quantity: 100,
    price: 2.89
  }
}
The Python version of this example is not currently available.
The Shell version of this example is not currently available.
Query metrics:
  •    bytesIn:    73

  •   bytesOut:   217

  • computeOps:     1

  •    readOps:     1

  •   writeOps:     1

  •  readBytes:    67

  • writeBytes: 1,013

  •  queryTime:  41ms

  •    retries:     0

The example runs successfully because the string avocados is present in the known_names index and 2.89 is a double. However, if we try to run the function with faulty inputs we see an error:

The C# version of this example is not currently available.
The Go version of this example is not currently available.
The Java version of this example is not currently available.
client.query(
  q.Call(q.Function('create_new_item'), ['bananas', 80, '1.89'])
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s\n%s',
  err.name,
  err.message,
  err.errors()[0].description,
  err.errors()[0].cause[0],
))
Error: ['BadRequest'] 'call error': 'Calling the function resulted in an error.'
{
  position: [ 'expr', 'in', 'else' ],
  code: 'transaction aborted',
  description: 'Name is unknown. Price is not a double. '
}
The Python version of this example is not currently available.
The Shell version of this example is not currently available.
Query metrics:
  •    bytesIn:  73

  •   bytesOut: 237

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   0

  •  readBytes:   0

  • writeBytes:   0

  •  queryTime: 5ms

  •    retries:   0

If, at some point in the future, we need to refine the create_new_item function, we can update it and continue to use our client code unmodified, as long as the function still accepts the same inputs. If, for example, we want to allow the function to run with the server role regardless of its caller’s role, we can update the role field of the UDF:

The C# version of this example is not currently available.
The Go version of this example is not currently available.
The Java version of this example is not currently available.
client.query(
  q.Update(
    q.Function('create_new_item'),
    { role: 'server' }
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s\n%s',
  err.name,
  err.message,
  err.errors()[0].description,
  err.errors()[0].cause[0],
))
{
  ref: Function("create_new_item"),
  ts: 1646262504310000,
  name: 'create_new_item',
  body: Query(Lambda(["name", "quantity", "price"], Let({"is_name_known": If(GT(Count(Match(Index("known_names"), Var("name"))), 0), true, false), "is_price_double": IsDouble(Var("price"))}, If(And(Var("is_name_known"), Var("is_price_double")), Create(Collection("products"), {"data": {"name": Var("name"), "quantity": Var("quantity"), "price": Var("price")}}), Abort(Concat([If(Var("is_name_known"), "", "Name is unknown. "), If(Var("is_price_double"), "", "Price is not a double. ")], "")))))),
  role: 'server'
}
The Python version of this example is not currently available.
The Shell version of this example is not currently available.
Query metrics:
  •    bytesIn:   79

  •   bytesOut:  847

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  557

  • writeBytes:  587

  •  queryTime: 22ms

  •    retries:    0

Limitations

  • A 30-second transaction timeout is imposed, which would typically be reached by a UDF that does not complete. When the timeout is reached, the transaction is terminated.

  • If a UDF causes a query process to exhaust available memory, the transaction is terminated.

  • Recursion is possible, but is limited to a depth of 200 calls.

  • In some contexts, such as within index bindings, using "server read-only" keys, or attribute-based access control, functions may be restricted from performing write or read operations.

Was this article helpful? 

We're sorry to hear that.
Tell us how we can improve!
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!