Skip to content
CHEATSHEET

Rel Cheatsheet

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

Foundations

Relations

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.

    liked_activity
    "Alice""hiking"
    "Alice""piano"
    "Bob""frisbee"
    "Charlie""hiking"
    "Charlie""chess"
    "Charlie""guitar"
  • Derived: contents computed from other relations based on logical rules.

    def music_lovers(x) {
        exists(y: liked_activity(x, y) and (y == "piano" or y == "guitar"))
    }
    music_lovers
    "Alice"
    "Charlie"

Queries

  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:

iddobjoin_datedept
11980-01-012010-01-01"HR"
21985-01-012015-01-01"Sales"

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

employee
:id11
:dob11980-01-01
:join_date12010-01-01
:dept1"HR"
:id22
:dob21985-01-01
:join_date22015-01-01
:dept2Sales

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:

table[{
    :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
}]
iddobjoin_datesalary
11980-01-012010-01-01100000
21985-01-012015-01-0180000

Language

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?

liked_activity["Alice"]
output
"hiking"
"piano"

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

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
output
"hiking"8
"frisbee"6
"piano1
"chess"2

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?

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

Rules

  • Rules give names to relations.
  • General form is 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.

Example:

def Person {
    "Alice"; "Bob"; "Charlie"
}
 
def Activity {
    "hiking"; "frisbee"; "piano"; "chess"; "guitar"
}
 
def free_activity(activity) {
    activity:cost[activity] = 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":

{ person : liked_activity(person, "hiking") }
output
"Alice"
"Charlie"

Alternative versions using from or for:

{ liked_activity(x, "hiking") for x in Person } // same as above
{ liked_activity(x, "hiking") from x in Person } // doesn't include the value of `x` in each tuple

Examples:

{ x^2 for x in range[1, 4, 1] }
output
11
24
39
416
{ x^2 from x in range[1, 4, 1] }
output
1
4
9
16

Bindings

  • Bindings introduce variables in Rel.

Examples:

  • 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 (person, activity) pair should be in the relation liked_activity if person likes activity and activity is strenuous:

      def liked_strenuous_activity(person in Person, activity in Person) {
          liked_activity(person, activity) and strenuousness[activity] > 5
      }
    2. In a relational abstraction:

      { person in Person : liked_activity(person, "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?

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

Example: Does everyone like hiking?

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.

Underscore

  • 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?

{ person: liked_activity(person, _) }
output
"Alice"
"Bob"
"Charlie"

Modules

Group relations under a common namespace.

module activity
    def strenuousness {
        "hiking", 8;
        "frisbee", 6;
        "piano", 1;
        "chess", 2
    }
 
    def cost {
        "hiking", 0;
        "frisbee", 0;
        "piano", 100;
        "chess", 0
    }
end
 
def output { activity }
output
:strenuousness"hiking"8
:strenuousness"frisbee"6
:strenuousness"piano"1
:strenuousness"chess"2
:cost"hiking"0
:cost"frisbee"0
:cost"piano"100
:cost"chess"0

Importing Module names

with activity use cost
 
def output {
    cost["piano"]
}
output
100

Parameterized Modules

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

@outline
module my_stats[R]
    def my_minmax = (min[R], max[R])
    def my_mean = mean[R]
    def my_median = median[R]
end
def output = my_stats[{1; 2; 3; 5; 8; 13; 100}]
output
:my_mean18.857
:my_median5.0
:my_minmax1 100

Bound Declarations

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

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".

Annotations

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).

Varargs

  • 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:

def second(y) {
    exists(x, z... : R(x, y, z...))
}
 
def R {
    1, 2, 3;
    4, 5, 6
}
def output { second }
output
2
5

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.

Example:

@inline
def mean[R] {
    sum[R] / count[R]
}
 
def output {
    mean[{5, 6, 10}]
}
output
7.0

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:

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*:
 
```rel
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:

entity type Person {
    String, String, Date
}
 
def person { ^Person["Alice", "Smith", 1980-01-01] }
def is_a_person { Person(person) }
def output { is_a_person }
// 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:

value type EmailAddress {
    String
}
 
def emailaddress = ^EmailAddress["you_and_i"]
 
// we can 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 S3 bucket with public read access.
  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 RelationalAI’s integration with Snowflake.
    • Requires that you have access to the 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.

Example:

load_csv["azure://raidocs.blob.core.windows.net/csv-import/simple-drinks.csv"]
output
country2Spain
country3Spain
country4Argentina
country5United States
country6Italy
drink2Gazpacho
drink3Sangría
drink4Yerba Mate
drink5Coca-Cola
drink6San Pellegrino

With a config relation:

module config
    def path {
        "azure://raidocs.blob.core.windows.net/csv-import/simple-drinks.csv"
    }
 
    module integration
        def provider = "azure"
        module credentials
            def azure_sas_token = raw"sv=2014-02-14&sr=example_token_0%3D"
        end
    end
 
    module schema
        def country = "string"
        def drink = "string"
        // other types:
        //     "int", "string", "float", "decimal(n, digits)", 
        //     "date", "datetime" and "bool"
    end
end
 
def output = load_csv[config]

With a string literal representing the CSV file contents:

module config
    def data {
        """
        country,drink
        Spain,Gazpacho
        Spain,Sangría
        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"
    end
end
 
def output = load_csv[config]

Loading JSON Files

Similar to loading a CSV file:

module config
    def data {
        """
        {
            "restaurant": ["McGee’s", "Paddy’s", "MacLaren’s"],
            "distance": [0.8, 7.6, 3.5]
        }
        """
    }
end
 
def output {
    load_json[config]
}
output
restaurant1McGee’s
restaurant2Paddy’s
restaurant3MacLaren’s
distance10.8
distance27.6
distance33.5
Was this doc helpful?