Officially out now: The TypeDB 3.0 Roadmap >>

TypeDB Fundamentals

Structured values for compound attributes



This article is part of our TypeDB 3.0 preview series. Sign up to our newsletter to stay up-to-date with future updates and webinars on the topic!

Users frequently find that their attributes do not hold a single primitive value, but instead combine multiple value into a single data structure. TypeDB 3.0 introduces structured value types, a.k.a. structs, which solve this problem once and for all: they allow user to defined their own compound value types.

In TypeDB’s type system, structs naturally complement lists: while lists combine values of a single type, struct allow us combine values by tupling multiple types into a single type.

At a glance: struct Syntax essentials

TypeDB 3.0 will allow users to define their own structs, comprising one or more labeled components with specified type T: this may be either a primitive value type (like bool or string) or another struct (like coordinate), either of which can be optional (like bool? or coordinate?). Relevant new syntax includes:

  • The keyword struct used for defining structs.
  • The keyword value for indicating the value type of a given struct component.
  • The suffix ? to indicate optionality of a component. The same suffix can also indicate optional return types in function definitions.

Now, let’s see some examples!

A simple struct

Structs are defined at schema-level using the keyword struct. To illustrate how this works, let’s define a simple coordinate struct (this may not be the best example, since coordinates are so important that they may deserve their own built-in type—but while we work on this, the example nicely illustrates that structs give all power to the user for defining what they need!).

define struct coordinate:
  latitude value double,
  longitude value double;

Like functions, after naming the struct, the “body” of the definition is preceded by a colon : . The body comprises a comma-separated list of the named components of the struct (in this case, latitude and longitude) and their respective values (both have type double above).

Using structs in the schema

After defining structs, they can be used like any other value type. For example, we may define a location attribute whose values are coordinates:

define 
  location sub attribute, value coordinate;
  city sub entity, owns location;

Inserting struct data

Below, we discuss how we can use struct-valued attribute in data queries. In general, using structs as data has many parallels to our earlier discussion of lists.

To begin, let us insert some struct data:

insert
  $berlin isa city, has location { longitude: 52.5200, latitude: 13.4050 };
  $london isa city, has location { longitude: 51.5072, latitude: 0.1276 };

This query inserts two cities, Berlin and London, with their respective location coordinates. As you can see, struct data is enclosed in curly parenthesis { ... } mirroring how structs are written in many other languages (their named field syntax { field: value } uniquely distinguishes them for our earlier stream types {T}, so that no major confusion will arise).

Reading struct data

Next, let’s see how to read struct data.

match
  $c1 isa city, has location $loc;
  { longitude: $long1, latitude: $lat1 } = $loc;
  $c2 isa city, has location { longitude: $long2, latitude: $lat2 };
  $long1 < $long2; 
fetch
  "southern city": $c1.name;
  "northern city": $c2.name;

In the first line, we match a city entity and its location attribute; this syntax is identical to the usual way we would match attributes of objects in TypeQL. In the second line we introduce two new variable $long1 and $lat1, and bind these variables by destructuring $loc.

Such a destructuring binding works, in principle, like any other value assignment statement in that it binds the value variables on the left-hand side to the expression on the right-hand side — of course, the novelty here is that the right-hand side is of struct type, and so we assign multiple variables on the left-hand side to its components. (See our the type-checking fundamentals for more on this.)

In fact, the first two lines in the match clause can be written in a more concise form, as illustrated by the third line. In the fourth line, we use the usual comparator < to compare to (primitive) values. (Note: comparators besides equality == are not automatically implemented for structs, though the user may define custom comparisons for their structs using functions. See the next section on combining functions and structs!)

Updating struct data

Updating structs works, similarly to lists, by replacing (i.e. “deleting and newly inserting”) struct values.

match
  $b isa city, has name "Berlin", has location { longitude: $x, latitude: $y };
insert
  $b has location { longitude: $x + 0.015, latitude: $y } @replace;

Again, our usage of @replace implicitly assumes that we specified that cities have at most one location (see the constraint fundamentals for more on this).

Combining functions and structs

Since struct are first-class citizens just like primitive value types we can, in particular, use them in functions. There is nothing deeply surprising about this, but just to see how powerful this combination can be, here is a very brief example.

with fun north_of($city: city) -> {city} :
  match
    $city has location { longitude: $l, latitude: $_ };
    $other_city has location { longitude: $j, latitude: $_ };
    $j > $l;
  return { $other_city };
match
  $berlin isa city, has name "Berlin";
  $other_city in north_of($berlin);
fetch
  "City north of Berlin": $other_city.name;

While for the example using a function is actually not strictly needed (i.e. we could have inlined the function directly in our query) it nicely illustrate how we can use functions to define custom comparisons for out structs: here, our query looks for all cities in our database that are north of $berlin.

Structs with other structs and optionals

Coordinates are a particularly simple struct. Let’s extend our simple example a little bit: in general, structs may both refer to other structs and contain optional fields (indicated by suffix ?). The following defines a new struct along these lines:

define 
  struct dated_coordinate:
    coord value coordinate;
    date value datetime;
    tried_local_beer value boolean?;

A dated_coordinate struct value is a tuple of a coordinate and an optional boolean. To use this struct in our schema let’s also define:

define
  traveler sub entity, owns travel_history[];
  travel_history sub attribute, value dated_coordinate;

Reading optionals optionally

Using nested structs works just as expected. Let’s check for travel_history attributes of travelers that pass through Berlin before 1989-12-22, without worrying whether they tried the local beer:

match
  $b isa city, has name "Berlin", has location $berlin_coord;
  $p isa traveler, has travel_history[] $visits;
  $visit in $visits;
  { coord: $berlin_coord, date: $date, tried_local_beer: $_ } = $visit;
  $date < 1989-12-22;

Note how in the query we first “unwind” the list of $visits by passing to members $visit in $visits , and then destructure the struct value on in line 5.

The above query will match travel_history attributes, whether or not they have tried_local_beer set: we indicated to TypeDB that we do not need that field to be set by using the anonymous variable $_. You may notice that this works exactly like accessing function results with optional return types as we saw earlier!

Reading optionals non-optionally

If we were to replace line 5 instead by:

{ coord: $berlin_coord, date: $date, tried_local_beer: $maybe } = $visit;

then we would match only those visits for which the tried_local_beer field has been set.

Summary

Structured value types give users the freedom to build the values they need for their attributes: coordinates, dated data points, multi-field texts, etc. Structs deeply integrate into the overall type system: they can be directly addressed from TypeQL patterns, and users may write custom functionality using TypeDB’s functions.

Share this article

TypeDB Newsletter

Stay up to date with the latest TypeDB announcements and events.

Subscribe to Newsletter

Further Learning

The TypeDB 3.0 Roadmap

The upcoming release of version 3.0 will represent a major milestone for TypeDB: it will bring about fundamental improvements to the architecture and feel, and incorporate pivotal insights from our research and user feedback.

Read article

Lists (3.0 Preview)

Lists are a core part of the functional database programming model, and address two issues in one go: storing data series and serializing data results!

Read article

Functions (3.0 Preview)

Functions provide powerful abstractions of query logic, which can be nested, recursed, or negated, and they natively embed into TypeQL's declarative patterns.

Read article

Feedback