Search...
ctrl/
Light
Dark
System
Sign in

Mutation rewrites

Mutation rewrites allow you to intercept database mutations (i.e., inserts and/or updates) and set the value of a property or link to the result of an expression you define. They can be defined in your schema.

Mutation rewrites are complementary to triggers. While triggers are unable to modify the triggering object, mutation rewrites are built for that purpose.

Here's an example of a mutation rewrite that updates a property of a Post type to reflect the time of the most recent modification:

Copy
type Post {
  required title: str;
  required body: str;
  modified: datetime {
    rewrite insert, update using (datetime_of_statement())
  }
}

Every time a Post is updated, the mutation rewrite will be triggered, updating the modified property:

Copy
db> 
... 
... 
insert Post {
  title := 'One wierd trick to fix all your spelling errors'
};
{default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}
Copy
db> 
select Post {title, modified};
{
  default::Post {
    title: 'One wierd trick to fix all your spelling errors',
    modified: <datetime>'2023-04-05T13:23:49.488335Z',
  },
}
Copy
db> 
... 
... 
update Post
filter .id = <uuid>'19e024dc-d3b5-11ed-968c-37f5d0159e5f'
set {title := 'One weird trick to fix all your spelling errors'};
{default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}
Copy
db> 
select Post {title, modified};
{
  default::Post {
    title: 'One weird trick to fix all your spelling errors',
    modified: <datetime>'2023-04-05T13:25:04.119641Z',
  },
}

In some cases, you will want different rewrites depending on the type of query. Here, we will add an insert rewrite and an update rewrite:

Copy
type Post {
  required title: str;
  required body: str;
  created: datetime {
    rewrite insert using (datetime_of_statement())
  }
  modified: datetime {
    rewrite update using (datetime_of_statement())
  }
}

With this schema, inserts will set the Post object's created property while updates will set the modified property:

Copy
db> 
... 
... 
insert Post {
  title := 'One wierd trick to fix all your spelling errors'
};
{default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}
Copy
db> 
select Post {title, created, modified};
{
  default::Post {
    title: 'One wierd trick to fix all your spelling errors',
    created: <datetime>'2023-04-05T13:23:49.488335Z',
    modified: {},
  },
}
Copy
db> 
... 
... 
update Post
filter .id = <uuid>'19e024dc-d3b5-11ed-968c-37f5d0159e5f'
set {title := 'One weird trick to fix all your spelling errors'};
{default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}
Copy
db> 
select Post {title, created, modified};
{
  default::Post {
    title: 'One weird trick to fix all your spelling errors',
    created: <datetime>'2023-04-05T13:23:49.488335Z',
    modified: <datetime>'2023-04-05T13:25:04.119641Z',
  },
}

Each property may have a single insert and a single update mutation rewrite rule, or they may have a single rule that covers both.

Inside the rewrite rule's expression, you have access to a few special values:

  • __subject__ refers to the object type with the new property and link values.

  • __specified__ is a named tuple with a key for each property or link in the type and a boolean value indicating whether this value was explicitly set in the mutation.

  • __old__ refers to the object type with the previous property and link values (available for update-only mutation rewrites).

Here are some examples of the special values in use. Maybe your blog hosts articles about particularly controversial topics. You could use __subject__ to enforce a "cooling off" period before publishing a blog post:

Copy
type Post {
  required title: str;
  required body: str;
  publish_time: datetime {
    rewrite insert, update using (
      __subject__.publish_time ?? datetime_of_statement() +
      cal::to_relative_duration(days := 10)
    )
  }
}

Here we take the post's publish_time if set or the time the statement is executed and add 10 days to it. That should give our authors time to consider if they want to make any changes before a post goes live.

You can omit __subject__ in many cases and achieve the same thing:

Copy
Show 2 hidden lines...
  required body: str;
  publish_time: datetime {
    rewrite insert, update using (
      __subject__.publish_time ?? datetime_of_statement() +
      .publish_time ?? datetime_of_statement() +
      cal::to_relative_duration(days := 10)
    )
  }
}

but only if the path prefix has not changed. In the following schema, for example, the __subject__ in the rewrite rule is required, because in the context of the nested select query, the leading dot resolves from the User path:

Copy
type Post {
  required title: str;
  required body: str;
  author_email: str;
  author_name: str {
    rewrite insert, update using (
      (select User {name} filter .email = __subject__.author_email).name
    )
  }
}
type User {
  name: str;
  email: str;
}

Learn more about how this works in our documentation on path resolution.

Using __specified__, we can determine which fields were specified in the mutation. This would allow us to track when a single property was last modified as in the title_modified property in this schema:

Copy
type Post {
  required title: str;
  required body: str;
  title_modified: datetime {
    rewrite update using (
      datetime_of_statement()
      if __specified__.title
      else __old__.title_modified
    )
  }
}

__specified__.title will be true if that value was set as part of the update, and this rewrite mutation rule will update title_modified to datetime_of_statement() in that case.

Another way you might use this is to set a default value but allow overriding:

Copy
type Post {
  required title: str;
  required body: str;
  modified: datetime {
    rewrite update using (
      datetime_of_statement()
      if not __specified__.modified
      else .modified
    )
  }
}

Here, we rewrite modified on updates to datetime_of_statement() unless modified was set in the update. In that case, we allow the specified value to be set. This is different from a default value because the rewrite happens on each update whereas a default value is applied only on insert of a new object.

One shortcoming in using __specified__ to decide whether to update the modified property is that we still don't know whether the value changed — only that it was specified in the query. It's possible the value specified was the same as the existing value. You'd need to check the value itself to decide if it has changed.

This is easy enough for a single value, but what if you want a global modified property that is updated only if any of the properties or links were changed? That could get cumbersome quickly for an object of any complexity.

Instead, you might try casting __subject__ and __old__ to json and comparing them:

Copy
type Post {
  required title: str;
  required body: str;
  modified: datetime {
    rewrite update using (
      datetime_of_statement()
      if <json>__subject__ {**} != <json>__old__ {**}
      else __old__.modified
    )
  }
}

Lastly, if we want to add an author property that can be set for each write and keep a history of all the authors, we can do this with the help of __old__:

Copy
type Post {
  required title: str;
  required body: str;
  author: str;
  all_authors: array<str> {
    default := <array<str>>[];
    rewrite update using (
      __old__.all_authors
      ++ [__subject__.author]
    );
  }
}

On insert, our all_authors property will get initialized to an empty array of strings. We will rewrite updates to concatenate that array with an array containing the new author value.

Mutation rewrites can be used to effectively create a cached computed value as demonstrated with the byline property in this schema:

Copy
type Post {
  required title: str;
  required body: str;
  author: str;
  created: datetime {
    rewrite insert using (datetime_of_statement())
  }
  byline: str {
    rewrite insert, update using (
      'by ' ++
      __subject__.author ++
      ' on ' ++
      to_str(__subject__.created, 'Mon DD, YYYY')
    )
  }
}

The byline property will be updated on each insert or update, but the value will not need to be calculated at read time like a proper computed property.

This section describes the syntax to declare mutation rewrites in your schema.

Define a new mutation rewrite corresponding to the more explicit DDL commands.

rewrite {insert | update} [, ...]
  using expr

Mutation rewrites must be defined inside a property or link block.

This declaration defines a new trigger with the following options:

insert | update [, ...]

The query type (or types) the rewrite runs on. Separate multiple values with commas to invoke the same rewrite for multiple types of queries.

expr

The expression to be evaluated to produce the new value of the property.

This section describes the low-level DDL commands for creatin and dropping mutation rewrites. You typically don't need to use these commands directly, but knowing about them is useful for reviewing migrations.

Define a new mutation rewrite.

When creating a new property or link:

{create | alter} type type-name "{"
  create { property | link } prop-or-link-name: type "{"
    create rewrite {insert | update} [, ...]
      using expr
  "}" ;
"}" ;

When altering an existing property or link:

{create | alter} type type-name "{"
  alter { property | link } prop-or-link-name "{"
    create rewrite {insert | update} [, ...]
      using expr
  "}" ;
"}" ;

The command create rewrite nested under create type or alter type and then under create property/link or alter property/link defines a new mutation rewrite for the given property or link on the given object.

type-name

The name (optionally module-qualified) of the type containing the rewrite.

prop-or-link-name

The name (optionally module-qualified) of the property or link being rewritten.

insert | update [, ...]

The query type (or types) that are rewritten. Separate multiple values with commas to invoke the same rewrite for multiple types of queries.

Declare two mutation rewrites on new properties: one that sets a created property when a new object is inserted and one that sets a modified property on each update:

Copy
alter type User {
  create property created: datetime {
    create rewrite insert using (datetime_of_statement());
  };
  create property modified: datetime {
    create rewrite update using (datetime_of_statement());
  };
};

Drop a mutation rewrite.

alter type type-name "{"
  alter property prop-or-link-name "{"
    drop rewrite {insert | update} ;
  "}" ;
"}" ;

The command drop rewrite inside an alter type block and further inside an alter property block removes the definition of an existing mutation rewrite on the specified property or link of the specified type.

type-name

The name (optionally module-qualified) of the type containing the rewrite.

prop-or-link-name

The name (optionally module-qualified) of the property or link being rewritten.

insert | update [, ...]

The query type (or types) that are rewritten. Separate multiple values with commas to invoke the same rewrite for multiple types of queries.

Remove the insert rewrite of the created property on the User type:

Copy
alter type User {
  alter property created {
    drop rewrite insert;
  };
};