Rel Data Types

This reference guide describes the various data types in Rel.


NumericPrimitiveSignedInt[8], SignedInt[16], SignedInt[32], SignedInt[64], SignedInt[128]Signed integers.
NumericPrimitiveUnsignedInt[8], UnsignedInt[16], UnsignedInt[32], UnsignedInt[64], UnsignedInt[128]Unsigned integers.
NumericPrimitiveFloating[16], Floating[32], Floating[64]Floating point numbers.
NumericPrimitiveRational[8], Rational[16], Rational[32], Rational[64], Rational[128]Rational numbers.
NumericPrimitiveFixedDecimal[nbits, ndecimals]Fixed-sized decimals with ndecimals digits of decimal precision and total bit size of nbits.
NumericAbstractNumberUnion of all numerical data types.
TextPrimitiveCharUTF-16 characters.
TextPrimitiveStringVariable-sized strings.
TimePrimitiveDateTimeTimestamps containing date and time information.
TimePrimitiveYear , Month , Week, DayDate periods.
TimePrimitiveHour, Minute, Second, Millisecond, Microsecond, NanosecondTime periods.
KeyPrimitiveHashHash value.
KeyPrimitiveAutoNumberInteger auto-numbers.
KeyAbstractEntityEntity key, union of Hash and AutoNumber.
MetaPrimitiveRelNameRelation names.
OtherAbstractAnyUnion of all data types.
OtherPrimitiveboolean_true, boolean_falseBoolean data type (for JSON support only).
OtherPrimitiveFilePosFile positions in a data file.
OtherPrimitiveMissingSingleton representing missing data.

We classify the data types based on their main purpose:

NumericNumerical data types.
TextText-based data types.
TimeTime-related data types.
KeyData types for identification purposes like UUIDs.
MetaData types referring to metadata information (like relation names).
OtherAll other supported data types.

We also distinguish between two kinds of data types:

PrimitiveData types with no further sub-types.
AbstractData types built on top of primitive types, usually unions of multiple primitive data types.


Relations are unordered collections of tuples. A tuple is an ordered collection of individual data values. Each data value has a data type.

In Rel, each data type has an associated unary relation, which includes all the values of that type. Type relations are often infinite, and therefore cannot be listed directly, but are still useful when specifying knowledge graphs, data schemas, and constraints.

Checking types is particularly useful for integrity constraints, as many of the examples in this document show. (See the concept guide on Integrity Constraints for more details.)

For each data type, Rel generally provides:

  1. A way to construct values of that data type,
  2. A type relation that tests if a given value belongs to that type, and
  3. Operations specific to the data type.

The following sections describe each supported data type and provide examples of how they can be used.

Numeric Data Types

Rel includes several numerical types, where the number of bits is specified to achieve a desired range and precision.

Ints and floats are 64-bit special cases of a more general set of fixed-width numerical types.


As a convenience, Number is a super-type that includes all the numerical types. It doesn’t come with its own constructor, meaning you can’t create this data type directly. You can only create directly primitive numerical data types.

Type Relation: Number(x).


def output  = {1; 1.5; pi_float64; decimal[64, 2, 3.14159]; rational[64, 2, 3]}

ic number_ic { subset(output, Number) }

Relation: output


Noteworthy Operations

Numerical operations are usually only compatible between values of the same type. When incompatible types are used, the result is the empty relation, false.

Operations include: +, -, *, ^, and /, also known as add, subtract, multiply, power, and divide.

Rel also offers bit-level operations between integers or unsigned integers of the same width:

Signed Integer

Integers are 64-bit signed integers, by default.


Integer constants and int[nbits, x] with bit size nbits, which can be 8, 16, 32, 64, or 128. Shortcut constructors: int64, int128.

def output = {1; -10; int[8, 5]; int[128, -10^10]}

Relation: output


Type Relation: Int(x), SignedInt(nbits, x)

For 64-bit integers (the default): Int(x).

For other bit-lengths: SignedInt(nbits, x).

def R = 1234; int[8, 5]

def output:a(x) = R(x) and (Int(x))
def output:b(x) = R(x) and (SignedInt(8, x))

Relation: output


Noteworthy Operations


def a = 432123432123432123
def output:overflow = a^2
def output:wide = int128[a]^2

Relation: output


Unsigned Integer


Unsigned integers can be constructed with

  • uint[nbits, x] with bit size nbits which can be 8, 16, 32, 64, or 128,
  • 0xNNNN for hexadecimal (base 16) constants,
  • 0oNNNN for octal (base 8) constants.

Hexadecimal and octal constants are assigned the unsigned integer with the lowest number of bits needed to represent it.

def output = {0xF; 0o77777777; 0xFFFF123FFFFF; uint[128, 1234]}

Relation: output


Shortcut constructors: uint64, uint128.

Type Relation: UInt(x) (64-bit)

UInt(x) holds iff x is a 64-bit integer.

Generalized type relation: UnsignedInt(nbits, x) with bit size nbits which can be 8, 16, 32, 64, or 128.

def R = {0xF; 0o77777777; uint128[1234]}

def output(x) = R(x) and (UInt(x) or UnsignedInt(32, x))

Relation: output


Noteworthy Operations

Mathematical operations between unsigned integers of different widths give an empty result.


def a = uint[64, 1000]
def b = uint[32, 100]
def uint_type[x] = UnsignedInt(16, x), "UInt16"
def uint_type[x] = UnsignedInt(32, x), "UInt32"
def uint_type[x] = UnsignedInt(64, x), "UInt64"
def uint_type[x] = UnsignedInt(128, x), "UInt128"
def output:sum = a + b // empty
def output:atype = uint_type[a]
def output:btype = uint_type[b]

Relation: output



Floats are 64-bit by default.


Float constants can be specified using a decimal point, or with scientific notation, as in 1e6 or 1.321E2.

To specify the bit-width, use the constructor float[nbits, x], for a float constant x with bit size nbits which can be 16, 32, or 64.

Shortcut constructor: float64[x].

def output = {2.0; float[32, 1.321]; 7.297e-3}

Relation: output


Type Relation: Float(x)

(Shortcut) Type relation (64-bit): Float(x), true iff x is a 64-bit float.

Generalized type relation: Floating(nbits, x). This works for 16, 32, and 64 bits.

def R = (2.0, float[32, 1.321])

ic float_ic {
subset(R, (Float, Floating[32]))

def output = R

Relation: output


Noteworthy Operations



Rational numbers are constructed with rational[nbits, numerator, denominator] for integer numerator and denominator. The supported bit sizes for nbits are: 8, 16, 32, 64, and 128.

def output = {rational[64, 2, 3]; rational[8, -1, 7]}

Relation: output


Type Relation : Rational(nbits, x).

The binary relation Rational(nbits, x) checks that x is of type Rational with a bit size of nbits.

def R = rational[16, -5, -7]

def output(x) = R(x) and Rational(16, x)

Relation: output


Noteworthy Operations


See also:


def x = rational[64, 200, 25]
def y = rational[64, 10, 3]

def output:fields = x, numerator[x], denominator[x]
def output:ops = x + y, x * y, x / y

Relation: output


We can add and multiply rationals by integers, but not by floats.

def x = rational[64, 1, 3]

def output = x * 3, x + 5

Relation: output



FixedDecimal numbers have two parameters:

  • decimals: the number of decimal digits (to the right of .), and
  • bits: the total number of bits used.

The following parameter combinations are supported:

bit sizedecimals $(d)$range
160–4$-2^{15}/10^d$ to $(2^{15}-1)/10^d$
320–8$-2^{31}/10^d$ to $(2^{31}-1)/10^d$
640–18$-(2^{63})/10^d$ to $(2^{63}-1)/10^d$
1280–37$-(2^{127})/10^d$ to $(2^{127}-1)/10^d$

You can use Rel to compute the range of a fixed decimal type:

def decimal_range[bits, decimals] =
-2^(bits-1)/10^(decimals), (2^(bits-1)-1)/10^(decimals)
def output = decimal_range[32, 4]

Relation: output



The constructor decimal[nbits, ndecimals, v] creates a fixed-size decimal for v.

def output = {decimal[32, 4, 2/3]; decimal[128, 20, sqrt[2]]}

Relation: output


Type Relation: FixedDecimal(nbits, ndecimals, x)

The ternary relation FixedDecimal(nbits, ndecimals, x) checks that x is of type FixedDecimal with a bit size of nbits and ndecimals decimal precision.

def R = {decimal[64, 4, pi_float64]; decimal[64, 10, pi_float64]}

def output(x) = R(x) and FixedDecimal(64, 4, x)

Relation: output


Noteworthy Operations

Operations between fixed-size decimals with different digits of precision yield an empty result.


def output:pi_3 = decimal[64, 3, 3.14159265359]
def output:sum_one = decimal[64, 1, 4.5] * 2
def output:sum_two = decimal[64, 0, 4.5] * 2

Relation: output

def d = parse_decimal[64, 2, "10.6"]

ic fixed_decimal_ic {FixedDecimal(64, 2, d)}

def output = d

Relation: output


Fixed decimals are particularly useful when you want to avoid floating-point errors, such as when dealing with financial data. For example:

def mydec[x] = decimal[64,2,x]
def output:float = 140.28 + 57.14 + 17.80
def output:fixed = mydec[140.28] + mydec[57.14] + mydec[17.80]

Relation: output


Text Data Types

Character (Char)

UTF-16 characters, thus allowing Unicode.


A character (char) has a Unicode character as its value and is specified with single quotes '.

def output = {'a'; '1'; 'α'; '文'; '👍'}

Relation: output


Type Relation: Char(x)

The unary relation Char(x) checks if x has type Char.

def R = {'C'; "abc"; 1}

def output(x) = R(x) and Char(x)

Relation: output


Noteworthy Operations


For a string s, char[s, i] is the i-th char in s, starting at 1:

def output = char["abc"]

Relation: output


Characters can be concatenated directly to strings with concat:

def output = concat['a', 'b'], concat['a', "bc"]

Relation: output



Strings are sequences of Rel chars (UTF-16 characters). The index of the first character is 1. Individual characters can be accessed with the char[str, i] relation.


String constants are enclosed in double quotes ". Multi-line string constants can be specified by using triple-double quotes ("""), which are also useful for including the double quote symbol inside a string. A quote can be escaped as ", and the \ character can be specified as “\”.

def output = {"abc"; "Michael Theodore \"Mickey\" Mouse"}

Relation: output

"Michael Theodore "Mickey" Mouse"

Multi-line string constant:

def output = """

Relation: output

"a,b,c 1,2,"three" "

Type Relation: String(x).

String(x) tests if x is a string.

def R = 1; 'a'; "b"; "abc"

def output(x) = R(x) and String(x)

Relation: output


Noteworthy Operations

char indexes characters in a string; substring[s, i, j] gives the substring between indices i and j inclusive.

def s = "abcde"

def output:first_char = char[s, 1]
def output:substring = substring[s, 2, 4]

Relation: output


The string relation converts a wide variety of other types into their string representation:

def output = string[1] ;
string[3.4] ;
string[unix_epoch] ;
string[Hour[1]] ;

ic string_ic { string[:a] = "a" }

Relation: output

"1 hour"


def output = concat["a", concat["b", string_trim["    c"]]]

Relation: output


Meta Data Types


RelName tests whether a value corresponds to a relation name. Relation names (aka RelName), when treated as data, start with the symbol :.

Strictly speaking, RelNames are metadata and result in specialized relations.

They are used extensively in modules — see the Modules Concept Guide. The column names of imported CSV files are also RelNames (see the FilePos Section for an example).


RelName constants start with a :.

def T = :bar

module T
def foo = true

def output = T

Relation: output


Type Relation: RelName(x)

RelName(x) tests whether x is a RelName.

def mymodule:f = 1

ic { subset(mymodule, (RelName, Int)) }

def output = mymodule

Relation: output


Noteworthy Operations

relname_string converts RelNames into strings:

def output:string = relname_string[:foo]

Relation: output



module myrel
def foo = 1
def myrel:bar = 2

ic relname_ic {subset(myrel, (RelName, Int))}

def names(name) = myrel(name, _)
ic names_ic { subset(names, RelName) }

def output = names

Relation: output


Entity Data Types

Type Relation: Entity(x)

The Entity relation holds if a value has an AutoNumber or Hash type, which are the types created by the entity declaration:

entity Planet planet_constructor = {"Earth"; "Venus"; "Mars"}

ic entity_ic {
subset(planet_constructor, {(String, Entity)})
ic constructor_ic {Entity(planet_constructor["Earth"])}

def output = Planet

Relation: output


See the Entity Concept Guide for more details.


The Hash type contains values generated by the hash128 utility, which includes entities generated with the (default) @hash annotation.


Hashes are constructed with hash128[R], which hashes each of the tuples in the relation and adds it as a new column in the result.

def output = hash128[{("a",1); ("b",2); ("c",3)}]

Relation: output

entity E e = 1;2;3

Relation: E


Type Relation: Hash(x).

Hash(x) tests whether x is a Hash.

def output = hash128[{("a",1); ("b",2); ("c",3)}]

ic hash_ic { subset(output:values, (String, Int, Hash)) }

Relation: output

entity E e = 1;2;3

ic { Hash(E) }

Noteworthy Operations

The hash_value_uint128_convert utility converts a Hash type to a Uint128:

def hashes = hash128[{"a" ; "b"}]
def output(x,y,z) = hashes(x,y) and z = hash_value_uint128_convert[y]

Relation: output


In the other direction, Uint128 values are converted to Hash with uint128_hash_value_convert:

def output = uint128_hash_value_convert[0x00000123456789abcdef]

Relation: output



The AutoNumber type contains values generated by the auto_number utility, which includes entities generated with the @auto_number annotation.


The auto_number[R] utility and @auto_number entities.

def output = auto_number[{"a"; "b"; "c"}]

Relation: output

@auto_number entity E e = 1;2;3

Relation: E


Type Relation: AutoNumber(x).

AutoNumber(x) tests whether x is an AutoNumber.

def output = auto_number[{"a"; "b"; "c"}]

ic auto_number_ic { subset(output, (String, AutoNumber)) }

Relation: output

@auto_number entity E e = 1;2;3

ic { AutoNumber(E) }

The basic date and datetime objects use the proleptic Gregorian calendar (which includes a year 0), and do not reference a timezone.

Date properties, such as date_year, do not require a timezone. We can think of dates as a unit of time, counted out day by day.

In contrast, datetime properties, such as datetime_year, do require a timezone. This is because we can interpret the 24-hour time component as relative to a timezone, giving different results for the year/month/day/hour/minute in some cases. (Notice that a datetime’s second field does not depend on the timezone, so datetime_second does not take one as an argument.) Finally, if we want to avoid timezone issues, and treat datetimes as a monotonically increasing sequence similar to dates, we can use the UTC timezone, which has no geographical restrictions or adjustments for daylight savings time.


Calendar date.


Date constants can be specified directly in YYYY-MM-DD format:

def output = {1776-07-04; 2020-02-29}

Relation: output


The utility lib{parse_date[string, format]} parses a string into a date, using the same formats accepted by the Julia language.

They are also populated by the “date” type when loading CSV files.

Type Relation: Date(x).

Date(x) tests whether x has the data type Date.

def d = 2021-12-14

ic { Date(d) }

Noteworthy Operations


See also:


def d = parse_date["1616-4-23", "Y-m-d"]

def output:tuple = date_year[d], date_month[d], date_day[d]

def output:monthname = date_monthname[d]
def output:dayname = date_dayname[d]
def output:week = date_week[d]
def output:dayofyear = date_dayofyear[d]
def output:dayofweek = date_dayofweek[d]

Relation: output


To compute the number of days between two dates:

def d1 = parse_date["2014-1-29", "Y-m-d"]
def d2 = parse_date["2014-2-28", "Y-m-d"]

def output = x : date_add[d1, x] = d2 and is_Day(x)

Relation: output

30 days


A point in time, timezone-agnostic, with nanosecond resolution. When working with datetimes, we do not consider timezones or leap seconds.


Datetimes can be specified directly as YYYY-MM-DDThh:mm:ss<timezone>, where <timezone> is Z, or + or - followed by hh:mm. For example:

def output = {

Relation: output


They can be parsed from strings with parse_datetime[string, format].

def output = {
parse_datetime["2018-06-12 13:00 +00:00", "YYYY-mm-dd HH:MM zzzz"];
parse_datetime["2018-03-11 01:00 America/New_York", "Y-m-d H:M Z"]

Relation: output


For instance, if your datetime is specified as y-m-dTH:M:S.sZ, some workaround is needed:

def date_string = "2022-01-02T18:07:27.963Z"
def date_trimmed = substring[date_string, 1, num_chars[date_string]-1]
def output = parse_datetime[date_trimmed, "y-m-dTH:M:S.s"]

In this example, the timezone Z needs to be removed before the parsing (y-m-dTH:M:S.s format is allowed).

Type Relation: DateTime(x)

DateTime(x) tests whether x has the data type DateTime.

def dt = 2021-10-12T01:22:31+10:00

ic {DateTime(dt)}

Noteworthy Operations


def dt = parse_datetime["2021-01-01 01:00:00", "Y-m-d H:M:S"]

def output:one = datetime_year[dt, "Europe/Berlin"]
def output:two = datetime_year[dt, "America/New_York"]
def output:three = datetime_year[dt, "-03:00"]

Relation: output


Note that the resolution of datetime_now is milliseconds, which will be sufficient for most use cases.

def datetime_to_milliseconds(datetime, v) =
datetime_to_nanoseconds(datetime, ns) and
v = trunc_divide[ns, 10^6]
from ns, vf

def dt = unix_epoch + Day[1]
def output = datetime_to_milliseconds[dt]

ic datetime_ic { output = 24 * 60 * 60 * 1000 }

Relation: output


Date and Time Periods


Time Period: Nanosecond[n], Microsecond[n], Millisecond[n], Second[n], Minute[n], Hour[n]

Date Period: Day[n], Week[n], Month[n], Year[n]

For all constructors the variable n is an integer indicating how many multiples of the time/date periods are requested.

def output = {Second[100]; Hour[2]; Day[1]; Month[24]}

Relation: output

24 months
2 hours
1 day
100 seconds

Type Relations

The following unary relations check whether a variable has the specific date-period-like or time-period-like data type.

Time Period: is_Nanosecond(x), is_Microsecond(x), is_Millisecond(x), is_Second(x), is_Minute(x), is_Hour(x),

def R = {Nanosecond[100]; Microsecond[10]; Minute[1]}

def output(x) = R(x) and (is_Nanosecond(x) or is_Minute(x))

Relation: output

100 nanoseconds
1 minute

Date Period: is_Day(x), is_Week(x), is_Month(x), is_Year(x).

def R = {Day[100]; Week[52]; Month[1]; Year[2000]}

def output(x) = R(x) and (is_Week(x) or is_Year(x))

Relation: output

52 weeks
2000 years

Noteworthy Operations


Time periods can be added or subtracted from dates and datetimes. Rel follows the Julia period-arithmetic conventions. Adding Year, Day, and Second periods to a date or datetime is straightforward, since these periods have a well-defined time duration.

Adding Month periods, on the other hand, is different. It usually advances to the same date (and time) in the corresponding new month, so that adding months to the 10th of a month always results in the 10th of another month. For leap years, however, we choose the 28th, as adding months to the 29th would give a non-existing date.

def output:jan = parse_date["2014-1-29", "Y-m-d"] // not a leap year
def output:feb = output:jan + Month[1]
def output:mar = output:feb + Month[2]

Relation: output


Compare to:

def output:jan = parse_date["2016-1-29", "Y-m-d"] // a leap year
def output:feb = output:jan + Month[1]
def output:mar = output:feb + Month[2]

Relation: output


Note that adding day and month periods is therefore not associative:

def d = parse_date["2014-1-29", "Y-m-d"]
def output:one = (d + Day[1]) + Month[1]
def output:two = (d + Month[1]) + Day[1]

Relation: output


Other Data Types


The type Any covers all possible values, and can be used as a wildcard to match any type:

Type Relation: Any(x).

Any(x) will be true for any x.


def R = (1, 3) ; (1, "foo")

ic any_ic {subset(R, (Int, Any) )}

def output = R

Relation: output



The relation true is represented by an empty tuple (), which is a relation of arity 0 and cardinality 1. The relation false is the empty relation {}, which has arity 0 and cardinality 0.

Rel does include, however, a separate Boolean data type that is useful for importing data that explicitly depends on boolean values — in particular, JSON data (see example below).


There are two constructors, one for each boolean value: boolean_true and boolean_false.

def output = {boolean_false; boolean_true}

Relation: output


Type Relation: Boolean(x)

Boolean(x) tests whether x has the data type Boolean.

def R = {(1, boolean_true); (2, true); (3, boolean_false); (4, false)}

def output(x, y) = R(x, y) and Boolean(y)

Relation: output


Noteworthy Operations


In the following example, the JSON true and false values are mapped to the Rel data type Boolean.

def joe = parse_json["""
{ "address": null, "name" : "JJ" , "person" : true , "company" : false }

ic { Boolean(joe:person) }
ic { boolean_and[joe:person, boolean_not[joe:company]] = boolean_true }
ic { joe:person = boolean_true }

def output = joe

Relation: output


Here is an example showing how and, or and not operations over the data types are written:

def a = boolean_true
def b = boolean_false

def output:not_and = boolean_not[boolean_and[a, b]]
def output:or = boolean_or[a, b]

Relation: output


Note that boolean_and, boolean_or, and boolean_not are distinctly different from the Rel key words and, or, and not. The former operate only on boolean data, whereas the latter are part of logic expressions like Conjunction, Disjunction, and Negation.



FilePos types are created when importing CSV files. They are used as keys when joining columns from the same row. Currently, a user can’t directly create this data type themselves.

Type Relation: FilePos(x)

FilePos(x) tests whether x has the data type FilePos.

def config:data="""

def csv = load_csv[config]

def output(p) = csv(_, p, _)

def csv_ic {
subset(csv, (RelName, FilePos, String))

Relation: output



def config:data="""

def output = table[load_csv[config]]

Relation: output


See the CSV import guide for more details.


Missing is a singleton type, containing only one value, missing.

It is used for data that requires an explicit representation of null data. For instance, JSON data requires an explicit representation for missing data (see example below). Data representation that is not fully normalized, such as Third normal form (3NF), also requires an explicit representation of missing data.

For data that is stored in the fully normalized Graph Normal Form (GNF), missing data points are simply represented by not stating the missing fact in the relation. GNF is the recommended way to model data and makes best use of the RKGMS query optimizer.


A missing data value is created by simply writing missing.

def output = missing

Relation: output


Type Relation: Missing(x)

Missing(x) tests whether x has the data type Missing.

def R = {("+1", '👍'); ("+10", missing); ("+100", '💯')}

def output(x, y) = R(x, y) and not Missing(y)

Relation: output



Parsing JSON data that contains explicit null data.

def json = parse_json[""" { "address": null, "name" : "JJ" } """]
ic missing_ic { Missing(json:address) }

def output = json

Relation: output