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.
// read query def music_lovers(x) { exists(y: liked_activity(x, y) and (y = "piano" or y = "guitar")) }
music_lovers
"Alice"
"Charlie"
Queries
- Read queries are evaluated and the contents of the relation
output
are returned. - Write queries are the same except that the contents of the relations
insert
anddelete
— if defined — are used to modify the base relations in the knowledge graph. - 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:
- Indivisibility of facts. Each relation contains at most one value (non-key) column. In other words, a tuple corresponds to a single, indivisible fact.
- 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:
id | dob | join_date | dept |
---|---|---|---|
1 | 1980-01-01 | 2010-01-01 | "HR" |
2 | 1985-01-01 | 2015-01-01 | "Sales" |
Stacking this table into the form (column_name, row_id, value) does produce a GNF relation:
employee | ||
---|---|---|
:id | 1 | 1 |
:dob | 1 | 1980-01-01 |
:join_date | 1 | 2010-01-01 |
:dept | 1 | "HR" |
:id | 2 | 2 |
:dob | 2 | 1985-01-01 |
:join_date | 2 | 2015-01-01 |
:dept | 2 | Sales |
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
::std::display::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)
}]
id | dob | join_date | salary |
---|---|---|---|
1 | 1980-01-01 | 2010-01-01 | 100000 |
2 | 1985-01-01 | 2015-01-01 | 80000 |
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
andFloat
literal:123
,3.14
,1e-5
.- Date and datetime literal:
2023-01-23
or2023-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{()}
andfalse
is{}
. - Cartesian product:
,
- Union:
;
- Ranges:
range[start, stop, step]
counts fromstart
tostop
in steps ofstep
. - Aggregations:
max
,min
,sum
,count
,mean
, andargmax
. - 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
liked_activity["Alice"]
output |
---|
"hiking" |
"piano" |
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
output | |
---|---|
"hiking" | 8 |
"frisbee" | 6 |
"piano | 1 |
"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
?
// read query
liked_activity("Alice", "hiking")
// Output: {()} (true)
Rules
- Rules give names to relations.
- Rules follow the syntax
def <head> { <body }
ordef <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:
// 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") }
output |
---|
"Alice" |
"Charlie" |
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 }
Examples:
// read query
{ x^2 for x in range[1, 4, 1] }
output | |
---|---|
1 | 1 |
2 | 4 |
3 | 9 |
4 | 16 |
// read query
{ 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 namedperson
. -
1
. A binding may be a constant. -
x
,y
,z
. Introduces three variables:x
,y
, andz
-
x in A, y
. Introducesx
andy
with the domain ofx
restricted toA
.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:
-
In the head of a rule: A
(p, a)
pair should be in the relationliked_activity
if Personp
likes activitya
anda
is strenuous:// read query def liked_strenuous_activity(p in Person, a in Activity) { liked_activity(p, a) and activity:strenuousness[a] > 5 }
-
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.
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?
// read query
{ person: liked_activity(person, _) }
output |
---|
"Alice" |
"Bob" |
"Charlie" |
Modules
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)
}
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
// read query
with activity use cost
def output = cost["piano"]
output |
---|
100 |
Parameterized Modules
Modules, like rules, can take one or more parameters:
// read query
@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_mean | 18.857 |
:my_median | 5.0 |
:my_minmax | 1 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 ofR
and the first column ofS
.- 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 inS
that start with some element inR
.- Example:
{ "Alice"; "Bob" } <: liked_activity
restrictsliked_activity
to the tuples that start with"Alice"
or"Bob"
.
- Left override
<++
R <++ S
contains all the tuples ofR
, plus all the tuples inS
whose key (defined as all of the elements except the last) is not inR
.- 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 thatperson
likes if nonempty; otherwise it evaluates to"none"
.
Annotations
Used to modify the behavior of definitions.
@inline
. The definition should be expanded inline wherever it is used.@outline
. Used for definitions that feature higher-order variables.@ondemand
. Prevents the relation from being fully materialized. Rel will compute only those parts that are actually used.@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
:
// 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
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:
// read query
@inline
def mean_value[R] = sum[R] / count[R]
def output = mean_value[{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:
// 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 tuplerest...
is inserted into the base relationrelation_name
. - If
delete
contains tuples of the form(:relation_name, rest...)
, then the tuplerest...
is deleted from the base relationrelation_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
- Load a file from an Azure Blob Storage container.
- Access via a SAS token if the data are private.
- Load a file from an AWS S3 bucket.
- Access via a key ID and a secret access key if the data are private.
- 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.
- Use the RAI Integration Services for Snowflake.
- Requires access to a RAI integration.
- 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:
// read query
load_csv["azure://raidocs.blob.core.windows.net/csv-import/simple-drinks.csv"]
output | ||
---|---|---|
country | 2 | Spain |
country | 3 | Spain |
country | 4 | Argentina |
country | 5 | United States |
country | 6 | Italy |
drink | 2 | Gazpacho |
drink | 3 | Sangría |
drink | 4 | Yerba Mate |
drink | 5 | Coca-Cola |
drink | 6 | San Pellegrino |
With a config
relation:
// read query
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:
// read query
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:
// read query
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 | ||
---|---|---|
restaurant | 1 | McGee’s |
restaurant | 2 | Paddy’s |
restaurant | 3 | MacLaren’s |
distance | 1 | 0.8 |
distance | 2 | 7.6 |
distance | 3 | 3.5 |