Skip to content

Language Primitives

/

Data types

Closures

SurrealQL Syntax
LET $parameter = |@parameters| @expression;

SurrealDB allows you to define anonymous functions. These functions can be used to encapsulate reusable logic and can be called from within your queries. Below are some examples demonstrating their capabilities:

-- Define an anonymous function that doubles a number
LET $double = |$n: number| $n * 2;
RETURN $double(2);  -- Returns 4

-- Define a function that concatenates two strings
LET $concat = |$a: string, $b: string| $a + $b;
RETURN $concat("Hello, ", "World!");  -- Returns "Hello, World!"
-- Define a function that greets a person
LET $greet = |$name: string| -> string { "Hello, " + $name + "!" };
RETURN $greet("Alice");   -- Returns "Hello, Alice!"

You can also enforce type constraints within your functions to prevent type mismatches:

-- Define a function with a return type
LET $to_upper = |$text: string| -> string { string::uppercase($text) };
RETURN $to_upper("hello");  -- Returns "HELLO"
RETURN $to_upper(123);      -- Error: type mismatch

-- Define a function that accepts only numbers
LET $square = |$num: number| $num * $num;
RETURN $square(4);    -- Returns 16
RETURN $square("4");  -- Error: type mismatch

Many of SurrealDB's functions allow a closure to be passed in, making it easy to use complex logic on a value or the elements of an array.

The chain function which performs an operation on a value before passing it on:

"Two"
    .replace("Two", "2")
    .chain(|$num| <number>$num * 1000);
Output
2000

We can see that the input to the .chain() method is indeed a closure by creating our own that is assigned to a parameter. This closure can be passed into .chain(), returning the same output as above.

LET $my_func = |$num| <number>$num * 1000;

"Two"
    .replace("Two", "2")
    .chain($my_func);

The following example shows a chain of array functions used to remove useless data, followed by a check to see if all items in the array match a certain condition, and then a cast into another type. The array::filter call in the middle ensures that the string::len function that follows is being called on string values.

[NONE, NONE, "good data", "Also good", "important", NULL]
    .filter(|$v| $v.is_string())
    .all(|$s| $s.len() > 5)
    .chain(|$v| <string>$v);
Output
'true'

Whether a closure can modify database resources depends on your version.

Closures work inside a read-only context, and cannot be used to modify database resources. This holds even when the write sits inside a function the closure calls.

-- 1. Create a test table and function
DEFINE TABLE test_table SCHEMAFULL;
DEFINE FIELD name ON test_table TYPE string;

DEFINE FUNCTION fn::test_create($name: string) -> object {
    CREATE test_table CONTENT { name: $name };
    { created: true, name: $name };
};

-- 2. Call the function directly - works
fn::test_create("direct_call");

-- 3. Call the function inside .map() - fails
LET $names = ["Alice", "Bob", "Charlie"];
$names.map(|$n| fn::test_create($n));
Output
Error: "Couldn't write to a read only transaction"

In many cases, a closure can be substituted by another operation such as a FOR loop or a regular SELECT statement.

DEFINE TABLE test_table SCHEMAFULL;
DEFINE FIELD name ON test_table TYPE string;

DEFINE FUNCTION fn::test_create($name: string) -> object {
    CREATE test_table CONTENT { name: $name };
    { created: true, name: $name };
};

-- Function to create a record called for each name
SELECT VALUE fn::test_create($this) FROM ["Alice", "Bob", "Charlie"];

Available since: v3.0.0

The original implementation of closures did not allow them to capture parameters (variables) in their scope. Strictly speaking, this made them simple anonymous functions as closures did not "enclose" anything.

LET $okay_nums = [1,2,3];

-- Returns [] because $okay_nums not present inside the closure
[1,5,6,7,0].filter(|$n| $n IN $okay_nums);

This has since been resolved, allowing a parameter declared outside a closure to be recognized inside it.

LET $okay_nums = [1,2,3];

[1,5,6,7,0].filter(|$n| $n IN $okay_nums);

An object field that holds a closure can be called with method syntax. $object.name(arguments) finds the field name on the object and calls the closure in it with the arguments given.

LET $vat = 0.2;
LET $pricing = {
	with_vat: |$net: number| $net * (1 + $vat),
	discounted: |$net: number, $percent: number| $net - $net * $percent / 100
};

$pricing.with_vat(100);
//- 120f

$pricing.discounted(80, 25);
//- 60

Built-in methods are resolved first. When objects have a built-in method of the same name, that method runs and the field is not called. len is a built-in method that returns the number of fields in an object, so it takes precedence over a len field:

LET $counter = { len: || 42, total: 7 };
$counter.len();
Output
2

A name that is neither a built-in method for objects nor a field holding a closure is an error:

{ x: 1 }.get('x');
Error output
"There was a problem running the get() function. no such method found for the object type"

Available since: v3.3.0

From 3.3.0 the closure body runs with the session's namespace and database, so it can query the database, and a body that writes makes the statement a write, so that the change persists as described in Closures and writes.

LET $people = {
	create: |$name: string| (CREATE ONLY person SET name = $name),
	total: || count(SELECT * FROM person)
};

$people.create('Aeon');
//- { id: person:ol0tw634oomrg4vfmwm3, name: 'Aeon' }

$people.total();
//- 1

The CREATE statement in the closure body is wrapped in parentheses, so that the comma after it ends the object field rather than continuing the SET clause.

Before 3.3.0, a closure field could not be called this way when its name matched a built-in method of another type, such as add or push for arrays. The call now falls back to the closure.

Warning

A closure reached through this fallback runs in a read-only context. It can query the database, but a write fails, including a write inside a function that the closure calls. Give a closure that writes a name that is not a built-in method of any type.

LET $people = {
	add: |$name: string| (CREATE ONLY person SET name = $name)
};

$people.add('Aeon');
Error output
"There was a problem with the key-value store: Couldn't write to a read only transaction"

Anonymous functions define small, reusable pieces of logic that can be used throughout your queries.

Was this page helpful?