Idioms
Idioms in SurrealQL provide a powerful and flexible way to access and manipulate data within records using paths. They allow you to navigate through nested data structures, access fields, array elements, call methods, and perform complex queries with ease. Idioms are similar to expressions in other query languages that provide a path to data within documents or records.
An idiom is composed of a sequence of parts that define the path to a value within a record or data structure. Each part specifies how to navigate to the next piece of data. Idioms can be used in various parts of SurrealQL. The most common usecase is in data retrival queries such as SELECT statements, but they can also be used in the WHERE clause, SET clause, and more.
An idiom is made up of one or more parts, each of which can be one of several types:
Field: Access a field by name.
Index: Access an element of an array by its index.
All: Access all elements or fields.
Last: Access the last element of an array.
Where: Filter elements based on a condition.
Method: Call a method on the current data.
Graph: Navigate through graph relationships.
Destructure: Destructure nested objects.
Optional: Indicate that the following part is optional.
Recurse: Recursively traverse paths such as graph and record links.
In this section, we'll explore each part in detail with examples to help you understand how to use idioms in SurrealQL.
Field Access
Since SurrealDB is a document database at its core, each record is stored on an underlying key-value store storage engine with the ability to store arbitrary arrays, objects, and many other types of data. To access a field in an object, use a dot . followed by the field name.
This is mostly helpful when accessing fields within a record, but can also be used to access fields within an array.
For example, using the CREATE statement to add a record into the person table:
To access the city field within the address object, you can use the following idiom:
In this example, person.name is an idiom that accesses the name field of the person record.
Index Access
To access an element in an array by its index, use square brackets [] with the index inside. For example, let's say we have a school record with some student results.
To access the first student in the results array, you can use the following idiom:
Here, results[0].score accesses the score of the first student in the results array.
All Elements
To access all elements in an array or all fields in an object, use .*. This is useful when you want to access all the elements in an array or all the fields in an object.
This idiom selects all elements in the score array.
The .* idiom is often seen in definitions and error messages.
The output for INFO FOR TABLE person includes an automatically generated definition for friends.*, namely every item inside the friends field.
Using .* to return values
The .* idiom in SurrealDB allows you to target all values in an object or all entries in an array. It can be used in various contexts such as querying, field definitions, and data manipulation. This section explains the behavior of .* with practical examples.
Accessing all values in an object
When applied to an object, .* returns an array containing all the values of the object's properties.
To see just the values of this object, the object::values() function can be used.
Defining Fields with .*
You can define fields using .* to specify constraints or types for all properties within an object field.
Here, we define a field obj of type object on the test table, and then specify that all properties within obj (obj.*) must be of type number.
With this done, attempting to insert a non-number value into any property of obj will result in an error.
Using .* in Different Contexts
Depending on where .* is used, it can have different effects on the order of operations.
For example, if we want to return all the properties of the person:tobie record, we can do the following:
Last Element
Addionally to access the last element of an array, use [$]. Refereing to the student record above, we can access the last element of the results array using the following idiom:
This idiom accesses the last element of the score array.
Method chaining
To call a method on the current data, use a dot . followed by the method name and parentheses () with arguments. SurrealDB supports method chaining, so you can call multiple methods (functions) on the same data. Learn more about method chaining in the functions section.
For example, let's create a new person record and then call uppercase() on its name field.
In the example above, uppercase() is a method called on person.name to convert it to uppercase. Although this method is called as .uppercase(), it is actually the string::uppercase() function that is called.
SurrealDB will automatically recognize that the idiom part .uppercase() refers to the string::uppercase() function and call this function when the query is executed. What this means is that the following two queries are equivilent:
To learn more about string method chaining in SurrealQL, see the string functions section.
Graph Navigation
SurrealDB can also be used in the context of graph databases, where data is stored and navigated using graph traversal idioms. The RELATE statement is used to create relationships between records. This allows you to traverse related records efficiently without needing to pull data from multiple tables and merging that data together using SQL JOINs.
For example, let's consider the following data:
Explanation:
*: Selects all fields ofexplorer:drake.->? AS actions: Retrieves all outgoing relationships from Drake and aliases them as actions.<-? AS was: Retrieves all incoming relationships to Drake and aliases them as was.<->? AS involved_in: Retrieves all relationships connected to Drake, regardless of direction, and aliases them asinvolved_in.
Destructuring
When working with nested data, you can destructure objects using the . and { ... } idioms.
For example,
You can also OMIT fields that you don't want to destructure using the OMIT clause.
Extending the example in the Graph Navigation section, we can use the -> idiom to navigate through the graph and destructure the city field.
Using aliases when destructuring
The keyword AS is necessary inside SELECT statements when using an alias (a new name for a field).
However, as destructuring involves defining the output shape of a new object, no AS keyword is needed. Instead, only the names of the fields are needed. Aliasing is done by choosing a new name, a : (colon) and the path to the value.
Conceptually, this is somewhat close to a RETURN statement.
Destructuring the current item in a SELECT query
The current record in a SELECT query can be accessed and destructured using the @ operator.
While the difference between the two methods is often cosmetic - aside from the note on aliases mentioned just above - using @ to access the current record does lead to a different style of query that may be preferable. While a regular SELECT query first returns an array of results that can then be operated on, a SELECT query that uses @ to access the current record can perform these operations first.
Most importantly, however, the @ operator is often necessary when using recursive paths.
Using expressions while destructuring
While the fields inside a destructuring operation have always been accessible, expressions were not. As of version 3.0.0.beta, this limitation no longer exists.
Expressions inside a destructuring operation have the same predefined parameters as any other expression, such as $this to the current object and $parent to the previous one.
Optional Parts
Note
The .? operator is used to indicate that a part is optional (it may not exist) it also allows you to safely access nested data without having to check if the nested data exists and exit an idiom path early when the result is NONE.
This idiom safely accesses person.spouse.name if spouse exists; otherwise, it returns NONE.
Using Optional Parts
If some person records have a spouse field and others do not:
This idiom will return NONE for spouse_name if the spouse field is not present.
Recursive paths
A recursive path allows record link or graph traversal down to a specified depth, as opposed to manually putting together a query to navigate down each level.
Using recursive graph traversal can be thought of as the equivalent of "show me all the third-generation descendants of Mr. Brown" as opposed to "show me the children and children's children and children's children's children of Mr. Brown".
The following shows a recursive query that returns the names of people known by records that the record person:tobie knows.
As the syntax of recursive queries tends to be complex to the untrained eye, this section will explain them in order of difficulty, beginning with what queries were necessary before recursive paths were added in SurrealDB version 2.1.
Overview
Take the following example that creates one planet, two countries, two states/provinces in each of these countries, and two cities in each of those states/provinces. The CREATE statements are followed by UPDATE statements to set record links between them, and RELATE to create bidirectional graph relations between them.
While traversing each of these paths can be done manually, it requires a good deal of typing and knowing the exact depth to traverse.
Here is an example using record links:
And here is an example using graph links.
Basics of recursive paths
Using a recursive path allows you to instead set the number of steps to follow instead of manually typing. A recursive path is made by isolating {} braces in between two dots, inside which the number of steps is indicated.
The number of steps can be any integer from 1 to 256.
A range can be inserted into the braces to indicate a desired minimum and maximum depth.
Using () to provide instructions at each depth
Parentheses can be added to a recursive query. To explain their use, consider the following example that attempts to traverse up to a depth of 3 and return the name of the records at that level.
Unfortunately, the output shows that the query stopped at a depth of one. This is because the query is instructing the database to recurse the entire ->has->(?).name path between 1 and 3 times, but after the first recursion it has reached a string. And a string on its own is of no use in a ->has->(?) graph query which expects a record ID.
In fact, the above query is equivalent to the following statement which encloses ->has->(?).name in parentheses.
To make the query work, we can shrink the area enclosed in the parentheses to ->has->(?), isolating the part to recurse before moving on to .name. It will repeat as many times as instructed and only then move on to the name field.
The syntax for the query above can be broken down as follows.
Using @ to refer to the current record
The @ symbol is used in recursive queries to refer to the current document. This is needed in recursive SELECT queries, as without it there is no way to know the context.
Adding @ allows the parser to know that the current planet record is the starting point for the rest of the query.
Using {} and .@ to combine results
Inside the structure of a recursive graph query, the @ symbol is used in the form of .@ at the end of a path to inform the database that this is the path to be repeated during the recursion. This allows not just the fields on the final depth of the query to be returned, but each one along the way as well.
The following two rules of thumb are a good way to understand how the syntax inside the structure of the query.
The individual fields inside a recursive query are simply populated at each point,
The field with
.@is used as the gateway to the next depth.
To see this visually, here is the unfolded output of the query above. The name and id fields appear at each point, while contains is used to move on to the next depth.
Similarly, only one .@ can be present inside such a query, as this is the path that is used to follow the recursive query until the end.
Here are some more simple examples of recursive queries and notes on the output they generate.
Behaviour of recursive queries
Recursive queries follow a few rules to determine how far to traverse and what to return. They are:
NONE,NULL, and arrays which are empty or contain onlyNONEand/orNULLare considered a dead end.An iteration with the same value as the previous one is also considered a dead end.
If an iteration with a dead end does not reach the minimum depth, it returns
NONE.If it has already passed the minimum depth, it returns the last valid value.
During each iteration, if it encounters an array value, all dead end values are automatically filtered out, ensuring no empty paths are included.
Filtering recursive fields
Recursive syntax is not just useful in creating recursive queries, but parsing them as well. Take the following example that creates some person records, gives each of them two friends, and then traverses the friends_with graph for the first person records to find its friends, friends of friends, and friends of friends of friends. Since every level except the last contains another connections field, adding a .{some_number}.connections to a RETURN statement is all that is needed to drill down to a certain depth.
Possible output of the final query:
Path and unique node collection, shortest path
SurrealDB has a number of built-in algorithms that allow recursive queries to collect all paths, all unique nodes, and to find the shortest path to a record. These can be used by adding the following keywords to the part of the recursive syntax that specifies the depth to recurse:
{..+path}: used to collect all walked paths.{..+collect}: used to collect all unique nodes walked.{..+shortest=record:id}: used to find the shortest path to a specified record id, such asperson:tobieorperson:one.
The originating (first) record is excluded from these paths by default. However, it can be included by adding +inclusive to the syntax above.
{..+path+inclusive}{..+collect+inclusive}{..+shortest=record:id+inclusive}
To demonstrate the output of these three algorithms, take the following example showing a small network of friends. The network begins with person:you, followed by two friends (person:friend1, person:friend2), then three acquaintances known by these friends (person:acquaintance1, person:acquaintance2, person:acquaintance3), and finally a movie star (person:star) who is known by only one of the acquaintances.
This representation of this small network of friends allows us to visualize the issues that these three algorithms solve. Using +path will output all of the possible paths from person:you, +collect will collect all of the records in this network, and +shortest=person:star will find the shortest path.
After specifying an algorithm to use, such as {..+path}, add the path that should be followed, in this case ->knows->person.
+path
Adding +path will output all of the possible paths starting from person:you.
+shortest
As the output of the previous example is fairly short, we can see that there are two ways to get from person:one to the movie star at person:star, one of which is one step shorter than the other.
To get the database to find the shortest path instead, change the algorithm to +shortest=person:star.
The part after +shortest can also take a parameter if it is a record ID. The following example will return the same result as the previous one.
+collect
Using +collect will collect all of the unique collected records. As this collection is created by moving recursively one level at a time, the output will show the closest connections first and least close connections at the end.
+inclusive
Adding +inclusive will show the same output, except that the original person:one record will also be present.
Other notes
The unbounded syntax .. can be replaced with a bounded range to ensure that the recursive query only goes down to a certain depth. For example, using ..2 with +collect will show all first- and second-degree relations starting from person:you:
Doing the same with +shortest=person:star will return an empty array, because there is no path from person:you to person:star that only requires two hops.
As shown in a previous section, parentheses can be used to show which path should be repeated during the recursion. After the path inside the parentheses, the destructuring operator, methods and so on can be used to modify the output. The query can also be written over multiple lines if desired.
Do not use .@ with algorithms
As these three methods use their own algorithms to follow a path, any attempt to construct your own path using .@ will result in an error. For example, choosing +path along with a field connections: ->knows->person.@ will return an error because +path on its own will use its own recursive planner to output every possible path as an array of arrays, while ->knowns->person.@ is an instruction to put together arrays of each record and the next result from the ->knows->person path at any possible depth.
Here is the output of both of these queries at a single depth to show the difference in output.
Example using record links
As is the case with other recursive queries, these three algorithms can be used in the same way with any other path that can be repeated, such as record links. The following example shows the same network of friends as the one above, except that it uses record links instead of graph queries. To traverse these paths, a simple .knows is all that is required.
Combining Idiom Parts
Idioms can combine multiple parts to navigate complex data structures seamlessly.
Suppose we have the following data:
To get the names of friends who are over 18:
Notes on Idioms
Chaining: Idioms can be chained to traverse deeply nested structures.
Performance: Be mindful of performance when using complex idioms; indexing fields can help.
NONE Safety: Use optional parts (
?) to handleNONEor missing data gracefully.Methods: Leverage built-in methods for data manipulation within idioms.
Type Casting: Use type casting if necessary to ensure data is in the correct format.
Best Practices
Use Destructuring: When selecting multiple fields, destructuring improves readability.
Limit Optional Parts: Use optional parts judiciously to avoid masking data issues.
Validate Data: Ensure data conforms to expected structures, especially when dealing with optional fields.
Index Fields: Index fields that are frequently accessed or used in
WHEREclauses for better performance.
Summary
Idioms in SurrealQL are a powerful tool for navigating and manipulating data within your database. By understanding and effectively using idiom parts, you can write expressive and efficient queries that handle complex data structures with ease. Whether you're accessing nested fields, filtering arrays, or traversing graph relationships, idioms provide the flexibility you need to interact with your data seamlessly.