Rel Data Types

This reference guide describes the various data types in Rel.

Overview

CategoryNameDescription
NumericPrimitiveSignedInt[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[32], Rational[64], Rational[128]Rational numbers.
NumericPrimitiveFixedDecimal[32, i], FixedDecimal[64, i], FixedDecimal[64, 8], FixedDecimal[128, i], FixedDecimal[128, 16]Fixed-sized decimals with i digits of precision, with $0\leq i \leq 5$.
NumericAbstractNumberUnion of all numerical data types.
TextPrimitiveCharUTF-16 characters.
TextPrimitiveStringVariable-sized strings.
TimePrimitiveDateTimeTimestamps containing date and time information.
TimePrimitiveDateDate.
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:

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

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

Introduction

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, which can be used to test 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 can be 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.

Number

As a convenience, Number is a super-type that includes all the numerical types.

Type Relation: Number(x).

Example:

query
def output  = {1.0 ; 1 ; pi_float64}
ic number_ic { subset(output, Number) }

Relation: output

1
1.0
3.141592653589793

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.

Construction

Integer constants, and int[nbits, x], for nbits in {8, 16, 32, 64, 128}.

Shortcut constructors: int64, int128.

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

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

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

Noteworthy Operations

Examples

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

Relation: output

:overflow-4321982966069415783
:wide186730660590134449234847760988287129

Unsigned Integer

Construction

uint[64, x], and hexadecimal 0xNNNN constants.

Hexadecimal constants (base 16, starting with 0x) and octal constants (base 8, starting with 0o) are assigned the smallest UInt width that fits them.

query
def a = 0xF
def b = 0o77777777
def c = 0xFFFF123FFFFF

def output = a, b, c

ic uint_ic1 { subset(a, UnsignedInt[8]) }
ic uint_ic2 { subset(b, UnsignedInt[32]) }
ic uint_ic3 { subset(c, UnsignedInt[64]) }

Relation: output

1516777215281470987927551

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), for nbits in {16; 32; 64; 128}.

Noteworthy Operations

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

Examples

query
def a = uint[64, 1000]
def b = uint[32, 100]
def uint_type[x] = UnsignedInt(16, x), "UInt16"
@inline
def uint_type[x] = UnsignedInt(32, x), "UInt32"
@inline
def uint_type[x] = UnsignedInt(64, x), "UInt64"
@inline
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

:atype"UInt64"
:btype"UInt32"

Float

Floats are 64-bit by default.

Construction

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, and nbits in {16 ,32, 64}.

Shortcut constructor: float64[x].

query
def output = (2.0, float[32, 1.321])
ic float_ic {
subset(output, (Float, Floating[32]))
}

Relation: output

2.01.321

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). Works for 16, 32, and 64 bits.

Noteworthy Operations

Rational

Construction

Constructor: rational[nbits, numerator, denominator], for integer numerator and denominator. Works for 8, 16, 32, 64, and 128 bits.

query
rational[64, 2, 3]

Relation: output

2//3

Type Relation : Rational(nbits, x).

query
def r = rational[32, 5,6]
def output("yes") = Rational(32, r)

Noteworthy Operations

Accessors:

See also:

Examples

query
def x = rational[64, 200, 25]
def output:fields = x, numerator[x], denominator[x]
def y = rational[64, 10, 3]
def output:ops = x + y, x * y, x / y

Relation: output

:fields8//181
:ops34//380//312//5

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

query
def x = rational[64, 1, 3]
def output = x * 3, x + 5

Relation: output

1//116//3

FixedDecimal

Fixed-size decimals, with a given bit-width, and up to 5 digits of precision.

Construction

Constructor: decimal[nbits, ndigits, v], for float or integer $v$.

Type Relation: FixedDecimal(nbits, ndigits, x)

FixedDecimal(nbits, ndigits, x), for $0 <= {\rm ndigits} <= 5$.

Noteworthy Operations

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

Examples

query
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

:pi_33.142
:sum_one9.0
:sum_two8
query
def d = parse_decimal[64, 2, "10.6"]
ic fixed_decimal_ic {FixedDecimal(64, 2, d)}
def output = d

Relation: output

10.60

Text Data Types

Character (Char)

UTF-16 characters, thus allowing Unicode.

Construction

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

query
def output = 'a'; '1'; 'α'; '文'; '👍'
ic { subset(output, Char) }

Relation: output

'1'
'a'
'α'
'文'
'👍'

Type Relation: Char(x)

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

query
x : Char(x) and {'C'; "abc"; 1}(x)

Relation: output

'C'

Noteworthy Operations

Examples

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

query
char["abc"]

Relation: output

1'a'
2'b'
3'c'

Characters can be also concatenated directly to strings with concat.

query
concat['a', 'b'], concat['a', "bc"]

Relation: output

"ab""abc"

String

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] function.

Construction

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 “\”.

query
def f1 = "abc"
def f2 = """
a,b,c
1,2,"three"
"""
def f3 = "Michael Theodore \"Mickey\" Mouse"
def output = f3

Relation: output

"Michael Theodore "Mickey" Mouse"

Type Relation: String(x).

String(x) tests if x is a string.

Noteworthy Operations

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

query
def s = "abcde"
def output:firstchar = char[s, 1]
def output:substring = substring[s, 2, 4]

Relation: output

:firstchar'a'
:substring"bcd"

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

query
def output = string[1] ;
string[3.4] ;
string[unix_epoch] ;
string[Hour[1]] ;
string[:a]

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

Relation: output

"1"
"1 hour"
"1970-01-01T00:00:00"
"3.4"
"a"

Examples

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

Relation: output

"abc"

Meta Data Types

RelName

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. Related, the column names of imported CSV files are RelNames (see the FilePos Section for an example).

Construction

RelName constants start with a :.

query
def T = :bar
module T
def foo = true
end
ic { subset(T, RelName) }

Relation: T

:bar
:foo

Type Relation: RelName(x)

query
def module:f = 1
ic { subset(module, (RelName, Int)) }

Noteworthy Operations

relname_string can be used to convert between strings and RelNames, in both directions:

query
def output:string = relname_string[:foo]
def output:name = x : relname_string(x, "bar")

Relation: output

:name:bar
:string"foo"

Examples

query
module myrel
def foo = 1
end
def myrel:bar = 2
ic relname_ic {subset(myrel, (RelName, Int))}
def names = name : myrel(name, x) from x
ic names_ic { subset(names, RelName) }
def output = names

Relation: output

:bar
:foo

Entity Data Types

Type Relation: Entity(x)

With the Entity type, we can test if a value is an entity, as created by the entity declaration.

query
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

RelationalAITypes.HashValue(0x3a4d6160f81d41c8a1890b0567da0b71)
RelationalAITypes.HashValue(0x5c23d8dd655371ad1d50588231fc9787)
RelationalAITypes.HashValue(0x6db02c98446209ef15a6bb0a71583a72)

See the Entity Concept Guide for more details.

Hash

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

Construction

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.

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

Relation: output

"a"1RelationalAITypes.HashValue(0xd5b955e6888e4165a67a4c01d7fbe677)
"b"2RelationalAITypes.HashValue(0x51f0ff0b384769c07900f53a5599f3ac)
"c"3RelationalAITypes.HashValue(0x54b3a81f69733969f5dfa46926f7a39b)
query
entity E e = 1;2;3

Relation: E

RelationalAITypes.HashValue(0x2d46a1ca9724b8fbe7ecb3be61ebe5c3)
RelationalAITypes.HashValue(0xa4e2d950f3a6d137f576e842147fe9da)
RelationalAITypes.HashValue(0xde49030d5787f7c0a9316fead1166a75)

Type Relation: Hash(x).

query
def output = hash128[{("a",1); ("b",2); ("c",3)}]
ic hash_ic { subset(output:values, (String, Int, Hash)) }

Relation: output

"a"1RelationalAITypes.HashValue(0xd5b955e6888e4165a67a4c01d7fbe677)
"b"2RelationalAITypes.HashValue(0x51f0ff0b384769c07900f53a5599f3ac)
"c"3RelationalAITypes.HashValue(0x54b3a81f69733969f5dfa46926f7a39b)
query
entity E e = 1;2;3
ic { Hash(E) }

AutoNumber

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

Construction

The auto_number[R] utility and @auto_number entities.

query
def output = auto_number[{"a"; "b"; "c"}]
ic auto_number_ic { subset(output, (String, AutoNumber)) }

Relation: output

"a"RelationalAITypes.AutoNumberValue(0x0000000000000001)
"b"RelationalAITypes.AutoNumberValue(0x0000000000000002)
"c"RelationalAITypes.AutoNumberValue(0x0000000000000003)
query
@auto_number entity E e = 1;2;3

Relation: E

RelationalAITypes.AutoNumberValue(0x0000000000000004)
RelationalAITypes.AutoNumberValue(0x0000000000000005)
RelationalAITypes.AutoNumberValue(0x0000000000000006)

Type Relation: AutoNumber(x).

query
def output = auto_number[{"a"; "b"; "c"}]
ic auto_number_ic { subset(output, (String, AutoNumber)) }

Relation: output

"a"RelationalAITypes.AutoNumberValue(0x0000000000000007)
"b"RelationalAITypes.AutoNumberValue(0x0000000000000008)
"c"RelationalAITypes.AutoNumberValue(0x0000000000000009)
query
@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.

Date

Calendar date.

Construction

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

query
def output = 2021-12-14

Relation: output

2021-12-14

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

query
def d = 2021-12-14
ic { Date(d) }

Noteworthy Operations

Accessors:

See also:

Examples

query
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

:dayname"Saturday"
:dayofweek6
:dayofyear114
:monthname"April"
:tuple1616423
:week16

To compute the number of days between two dates:

query
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

Datetime

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

Construction

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

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

Relation: output

2021-10-11T15:22:31

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

Type Relation: DateTime(x)

query
def dt = 2021-10-12T01:22:31+10:00
ic {DateTime(dt)}

Noteworthy Operations

Examples

query
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

:one2021
:three2020
:two2020

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

query
@inline
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

86400000

Date and Time Periods

Construction

Nanosecond, Microsecond, Millisecond, Second, Minute, Hour, Day, Week, Month, Year, all taking an integer argument.

Type Relations

is_Nanosecond(x), is_Microsecond(x), is_Millisecond(x), is_Second(x), is_Minute(x), is_Hour(x), is_Day(x), is_Week(x), is_Month(x), is_Year(x).

Noteworthy Operations

Examples

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.

query
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

:feb2014-02-28
:jan2014-01-29
:mar2014-04-28

Compare to:

query
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

:feb2016-02-29
:jan2016-01-29
:mar2016-04-29

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

query
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

:one2014-02-28
:two2014-03-01

Other Data Types

Any

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.

Example

query
def myrel = (1, 3) ; (1, :foo)
ic any_ic {subset(myrel, (Int, Any) )}

Relation: myrel

13
1:foo

Boolean

In Rel, true and false do not have a “boolean type”. true is {()}, the relation of arity 0 that contains a single tuple, and false is the empty relation of arity 0, {}.

Rel does include, however, separate Boolean types that are useful to import and encode certain kinds of information, JSON files in particular. For example:

query
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

:addressmissing
:companyfalse
:name"JJ"
:persontrue

We recommend that you only use these boolean types when strictly necessary.

Construction

boolean_true , boolean_false.

Type Relation: Boolean(x)

Noteworthy Operations

Examples

query
def a = boolean_true
def b = boolean_false
def output:and = boolean_and[a,b]
def output:or = boolean_or[a,b]

Relation: output

:andfalse
:ortrue

FilePos

Construction

FilePos types are created when importing CSV files. They are used as keys when joining columns from the same row.

Type Relation: FilePos(x)

Examples

query
def config:data="""
a,b,c
1,2,3
4,5,6
"""
def csv = load_csv[config]
def output = p : csv(_, p, _)
def csv_ic {
subset(csv, (RelName, FilePos, String))
}

Relation: output

RelationalAITypes.FilePos(7)
RelationalAITypes.FilePos(13)

See the CSV import guide for more details.

Missing

Missing is a singleton type, containing only one value, missing. It is useful, in particular, to represent data that is null or missing when importing JSON or CSV data. (Note, however, that in general we prefer to simply let missing keys have a value of false, that is, not represent them at all, whenever possible.)

Construction

The singleton constant missing, as in def x = missing.

query
def x = missing
def output("yes") = Missing(x)

Relation: output

"yes"

Type Relation: Missing(x)

Examples

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

Relation: output

:addressmissing
:name"JJ"