Skip to content

Rel Cheatsheet

This cheatsheet provides a quick reference for the Rel language and the RelationalAI Knowledge Graph System.



Every value in Rel is a relation, which is an unordered set of tuples.

A relation can be either a base relation or a derived relation:

  • Base: contents declared to the knowledge graph directly.

  • Derived: contents computed from other relations based on logical rules.

    // read query
    def music_lovers(x) {
        exists(y: liked_activity(x, y) and (y = "piano" or y = "guitar"))


  1. Read queries are evaluated and the contents of the relation output are returned.
  2. Write queries are the same except that the contents of the relations insert and delete — if defined — are used to modify the base relations in the knowledge graph.
  3. A model is a collection of declarations in Rel. Loading a model means putting the relations defined in the model into the database and making them available to all subsequent queries.

Data Modeling

Graph Normal Form

Data in a RelationalAI knowledge graph are represented in Graph Normal Form:

  1. Indivisibility of facts. Each relation contains at most one value (non-key) column. In other words, a tuple corresponds to a single, indivisible fact.
  2. Things not strings. Data values in the database that are meant to represent the same real-world referent are equal in the database.

The value column of a relation, if there is one, should be the last column.

Example: This table is not in GNF because each row contains several independent facts:


Stacking this table into the form (column_name, row_id, value) does produce a GNF relation:


Displaying Tables

The relation table can be applied to a relation of the form (column_name, row_id, value) to unstack it and display it in the traditional wide form:

// read query
    (:id, 1, 1);
    (:dob, 1, 1980-01-01);
    (:join_date, 1, 2010-01-01);
    (:salary, 1, 100000);
    (:id, 2, 2);
    (:dob, 2, 1985-01-01);
    (:join_date, 2, 2015-01-01);
    (:salary, 2, 80000)


Syntax and Basic Operations

  • Identifier: is a sequence of characters that begins with a Unicode letter, an underscore or a caret (^).
  • String literals: "Alice" or """Alice""" (multi-line).
  • String interpolation: "Hi %(name)!"
  • Raw strings (no interpolation): raw"30% increase"
  • Character literal: 'A'.
  • Int and Float literal: 123, 3.14, 1e-5.
  • Date and datetime literal: 2023-01-23 or 2023-01-23T12:34:56.
  • Symbol literal: :strenuousness. Symbols are like strings but are used for values that represent schema rather than data.
  • Conditional expression: if 1 < 2 then "A" else "B" end
  • Math operators: +, -, *, /, %, ^.
  • Comparison operators: =, or !=, <, >, or <=, or >=.
  • Boolean operators: and, or, not.
  • Boolean values: true is {()} and false is {}.
  • Cartesian product: ,
  • Union: ;
  • Ranges: range[start, stop, step] counts from start to stop in steps of step.
  • Aggregations: max, min, sum, count, mean, and argmax.
  • Comments: // (single-line) or /* */ (block).

Partial Relational Application

Find the rest of each tuple in a relation that starts with a given element or elements.

Example: What activities does Alice like?

// read query

Brackets may be omitted in expressions of the form `relation_name[:symbol].

// read query
def activity = {
    (:strenuousness, "hiking", 8);
    (:strenuousness, "frisbee", 6);
    (:strenuousness, "piano", 1);
    (:strenuousness, "chess", 2);
    (:cost, "hiking", 0);
    (:cost, "frisbee", 0);
    (:cost, "piano", 100);
    (:cost, "chess", 0)
def output = activity:strenuousness

Relational Application

  • A partial application with all arguments.
  • Uses parentheses instead of square brackets.

Example: Is the tuple ("Alice", "hiking") in the relation liked_activity?

// read query
liked_activity("Alice", "hiking")
// Output: {()} (true)


  • Rules give names to relations.
  • Rules follow the syntax def <head> { <body } or def <head> = <body>.
  • A rule says that any tuple in the body must also be in the head.
  • A parameterized rule introduces one or more variables in its head. It says that for every assignment of values to the parameters, any tuple in the body must also be in the head.


// read query
def Person = {"Alice"; "Bob"; "Charlie"}
def Activity = {"hiking"; "frisbee"; "piano"; "chess"; "guitar"}
def free_activity(activity_id) {
    activity:cost[activity_id] = 0
def output = free_activity("chess")
// Output: () // True

Relational Abstraction

Define a relation anonymously based on a logical condition.

Example: the set containing every element person such that person likes the activity "hiking":

// read query
{ person : liked_activity(person, "hiking") }

Alternative versions using for:

// read query
{ liked_activity(x, "hiking") for x in Person }

and from:

// read query
{ x, liked_activity(x, "hiking") from x in Person }


// read query
{ x^2 for x in range[1, 4, 1] }
// read query
{ x^2 from x in range[1, 4, 1] }


  • Bindings introduce variables in Rel.


  • person. Introduces a single variable named person.

  • 1. A binding may be a constant.

  • x, y, z. Introduces three variables: x, y, and z

  • x in A, y. Introduces x and y with the domain of x restricted to A. in clauses go with the variable they restrict.

  • x ∈ Red, y ∈ Green where edge(x, y). where clauses go at the end of the binding.

  • Bindings may appear:

    1. In the head of a rule: A (p, a) pair should be in the relation liked_activity if Person p likes activity a and a is strenuous:

      // read query
      def liked_strenuous_activity(p in Person, a in Activity) {
          liked_activity(p, a) and activity:strenuousness[a] > 5
    2. In a relational abstraction:

      // read query
      { p in Person : liked_activity(p, "hiking") }

Existential and Universal Quantification

  • exists is used to express existential quantification.
  • forall is used to express universal quantification.

Example: Is there anyone who likes hiking?

// read query
exists(x : liked_activity(x, "hiking"))
// Output: {()} (true)

Example: Does everyone like hiking?

// read query
forall(person in Person : liked_activity(person, "hiking"))
// Output: {} (false)

Note: <expression> from <binding> is equivalent to exists(<binding> : <expression>), so from is an alternative way to express existential quantification.


  • Used like a wildcard in relations.
  • A relational application involving wildcards evaluates to true if there exist elements that could be substituted for the wildcards to make the application true.

Example: Who are all the people who like at least one activity?

// read query
{ person: liked_activity(person, _) }


Group relations under a common namespace.

// read query
module activity
    def strenuousness = {
        ("hiking", 8);
        ("frisbee", 6);
        ("piano", 1);
        ("chess", 2)
    def cost = {
        ("hiking", 0);
        ("frisbee", 0);
        ("piano", 100);
        ("chess", 0)
def output = activity

Importing Module names

// read query
with activity use cost
def output = cost["piano"]

Parameterized Modules

Modules, like rules, can take one or more parameters:

// read query
module my_stats[R]
    def my_minmax = (min[R], max[R])
    def my_mean = mean[R]
    def my_median = median[R]
def output = my_stats[{1; 2; 3; 5; 8; 13; 100}]
:my_minmax1 100

Bound Declarations

A bound declaration tells the compiler that a relation with a given name exists and may constrain the tuples:

// read query
from ::std::datetime import Date
bound due_date = Entity, Date
def due_date = {
    ^Person["Alice", "Smith", 1980-01-01], 2024-08-24;
    ^Person["Bob", "Wilson", 1962-05-12],  2025-07-16

Advanced Features

Special Join Operators

  • Composition .
    • R.S is an equality join on the last column of R and the first column of S.
    • Example: liked_activity.(activity:strenuousness) maps each person to the strenuousness values of the activities they like.
  • Prefix join <:
    • R <: S is the set of tuples in S that start with some element in R.
    • Example: { "Alice"; "Bob" } <: liked_activity restricts liked_activity to the tuples that start with "Alice" or "Bob".
  • Left override <++
    • R <++ S contains all the tuples of R, plus all the tuples in S whose key (defined as all of the elements except the last) is not in R.
    • Example: {(1, 2), (3, 4)} <++ {(1, 5)} evaluates to {(1, 2), (3, 4)}, and {(1, 2), (3, 4)} <++ {(5, 6)} evaluates to {(1, 2), (3, 4), (5, 6)}.
    • Example: if person is a variable, liked_activity[person] <++ "none" evaluates to the set of activities that person likes if nonempty; otherwise it evaluates to "none".


Used to modify the behavior of definitions.

  1. @inline. The definition should be expanded inline wherever it is used.
  2. @outline. Used for definitions that feature higher-order variables.
  3. @ondemand. Prevents the relation from being fully materialized. Rel will compute only those parts that are actually used.
  4. @static. Used for integrity constraints that can be evaluated directly from schema (that is, without referring to data).


  • Represents a sequence of zero or more arguments.
  • Denoted by a variable name followed by ....

Example: Get the elements in the second column of the relation R:

// read query
def second_element(y) {
    exists(x, z... : R(x, y, z...))
def R = {(1, 2, 3); (4, 5, 6)}
def output = second_element

Higher-order Definitions

  • Definitions of relations whose arguments can represent relations rather than individual elements.
  • Names of arguments that represent relations must begin with a capital letter.


// read query
def mean_value[R] = sum[R] / count[R]
def output = mean_value[{5, 6, 10}]

Integrity Constraints

A declaration that ensures that a specified condition is met. If the body of an integrity constraint evaluates to false, the transaction in which it is evaluated is aborted.

Integrity constraints may be used in a read or write query or in a model loaded in the database.

Example: Every liked activity should have a strenuousness score:

// read query
ic activity_has_strenuousness(p in Person, a in Activity) {
    liked_activity(p, a) implies activity:strenuousness(a, _)

Example: Every rating is between 0 and 5 inclusive:

// read query
ic rating_in_bounds(p in Person, a in Activity) {
    liked_activity(p, a) implies 0 <= rating[p, a] <= 5

Note: F implies G is equivalent to not F or G. Putting F implies G in an integrity constraint ensures that the relation F and not G is empty.

Entities and Value Types

An entity exists in the real world and can be identified independently of any specific properties. Examples include persons, companies, and nations.

A value type is inextricably linked to its identifying data. Examples include phone numbers, email addresses, unitful measures (for example, quantities denominated in meters or kilograms).

Entity example Entity produces a hash value that can be used to identify the entity:

// read query
from ::std::datetime import Date
entity type Person = String, String, Date
def alice = ^Person["Alice", "Smith", 1980-01-01]
def output = Person(alice)
//output> ()  // True

The relation that begins with a hat (^) is a constructor for the entity type Person. The entity type Person is an ordinary relation; it maps entities to true or false depending on whether or not the entity is a person.

Value type example:

// read query
value type EmailAddress = String
def emailaddress = ^EmailAddress["you_and_i"]
// Use the constructor to access the underlying string.
def is_valid(email in EmailAddress) {
    ^EmailAddress[address_string] = email
    and count[string_split["@", address_string]] = 2
    from address_string

Control Relations

Control relations are treated as ordinary relations by the language, and the query engine uses their contents to produce various effects:

  • If insert contains tuples of the form (:relation_name, rest...), then the tuple rest... is inserted into the base relation relation_name.
  • If delete contains tuples of the form (:relation_name, rest...), then the tuple rest... is deleted from the base relation relation_name. Deletions are effected before insertions.
  • If abort contains () in a given transaction, the transaction is aborted.
  • If output contains tuples, those tuples are returned as the result of the transaction.
  • If export contains tuples, those tuples are exported to the specified destination.
  • If rel:config contains tuples, they are used to customize system behaviors.

Data Loading

Ways to Load Data

  1. Load a file from an Azure Blob Storage container.
    • Access via a SAS token if the data are private.
  2. Load a file from an AWS S3 bucket.
    • Access via a key ID and a secret access key if the data are private.
  3. Upload a file from your computer.
    • The file must be less than 64 MB.
    • Each frontend (Console, CLI, VS Code Extension, SDKs) has an upload feature.
  4. Use the RAI Integration Services for Snowflake.
    • Requires access to a RAI integration.
  5. Supply the data as a CSV- or JSON-formatted string literal in Rel source code.

Loading CSV Files

To load a CSV or JSON file, provide the file path or a configuration relation.


// read query
country5United States
drink4Yerba Mate
drink6San Pellegrino

With a config relation:

// read query
module config
    def path {
    module integration
        def provider = "azure"
        module credentials
            def azure_sas_token = raw"sv=2014-02-14&sr=example_token_0%3D"
    module schema
        def country = "string"
        def drink = "string"
        // Other types:
        //     "int", "string", "float", "decimal(n, digits)",
        //     "date", "datetime" and "bool"
def output = load_csv[config]

With a string literal representing the CSV file contents:

// read query
module config
    def data {
        Argentina,Yerba Mate
        United States,Coca-Cola
        Italy,San Pellegrino
    module schema
        def country = "string"
        def drink = "string"
        // Other types:
        //     "int", "string", "float", "decimal(n, digits)",
        //     "date", "datetime" and "bool"
def output = load_csv[config]

Loading JSON Files

Similar to loading a CSV file:

// read query
module config
    def data {
            "restaurant": ["McGee’s", "Paddy’s", "MacLaren’s"],
            "distance": [0.8, 7.6, 3.5]
def output = load_json[config]
Was this doc helpful?