Skip to main content

Module definitions

Entity

You store values (instances) of an entity in Rell in a database, not in memory. You can create or delete them explicitly using the Rell create and delete expressions. An in-memory equivalent of an entity in Rell is a struct.

A variable of an entity type holds an ID (primary key) of the corresponding database record, but not its attribute values.

entity company {
name: text;
address: text;
}

entity user {
first_name: text;
last_name: text;
year_of_birth: integer;
mutable salary: integer;
}

xxxxxxxxxx print("Hello"/, "World"/);/print("Bye");/rell

entity user {
name; // built-in type "name"
company; // user-defined type "company" (error if no such type)
}

Attributes may have default values:

entity user {
home_city: text = 'New York';
}

You can use rowid implicit attribute (of type rowid) to get the ID (database primary key) of an entity value.

val u = user @ { .name == 'Bob' };
print(u.rowid);

val alice_id = user @ { .name == 'Alice' } ( .rowid );
print(alice_id);

Keys and indices

Entities can have key and index clauses:

entity user {
name: text;
address: text;
key name;
index address;
}

Keys and indices may have multiple attributes:

entity user {
first_name: text;
last_name: text;
key first_name, last_name;
}

You can specify mutability within a key or index clause. Here one can also set a default value:

entity address{
index mutable city: text = 'Rome';
}

You can combine attribute definitions with key or index clauses, but such definition has restrictions (can't specify mutable):

entity user {
key first_name: text, last_name: text;
index address: text;
}

Entity annotations

@log entity user {
name: text;
}

The @log annotation has following effects:

  • adds a special attribute transaction of type transaction to the entity
  • result of op_context.transaction (current transaction) sets the transaction value
  • entity can't have mutable attributes
  • can't delete the values

Changing existing entities

When you start a Rell app, database structure update happens: tables for new entities and objects get created and tables for existing ones gets altered. There are limitations on changes that you can make in the existing entities and objects.

What's allowed:

  • Adding an attribute with a default value (a column gets added to the table and initialized with the default value).
  • Adding an attribute without a default value - only if there are no records in the table.
  • Removing an attribute (database column isn't dropped; you can read the attribute later).

What's not allowed:

  • Any changes in keys/indices, including adding a new key/index attribute, making an existing attribute into key/index, removing an attribute from an index, etc
  • Changing attribute's type
  • Adding/removing the @log annotation

Object

Object is similar to entity, but there can be only one instance of an object:

object event_stats {
mutable event_count: integer = 0;
mutable last_event: text = 'n/a';
}

Reading object attributes

query get_event_count() = event_stats.event_count;

Modifying an object

operation process_event(event: text) {
update event_stats ( event_count += 1, last_event = event );
}

Features of objects

  • Like entities, objects get stored in a database.
  • Objects get initialized automatically during blockchain initialization.
  • Can't create or delete an object from code.
  • Attributes of an object must have default values.

Struct

Struct is similar to entity, but its values exist in memory, not in a database.

struct user {
name: text;
address: text;
mutable balance: integer = 0;
}

Features of structs

  • Attributes are immutable by default, and only mutable when declared with mutable keyword.
  • Attributes can have
  • An attribute might have a default value that's used if the attribute isn't specified during construction.
  • Structs get deleted from memory implicitly by a garbage collector.

Creating struct values

val u = user(name = 'Bob', address = 'New York');

You can use .to_struct()to create a struct-copy of an entity or an object.

entity user{
name;
address: text;
}

val u = user @ {.name == 'Bob'};
val s = u.to_struct(); // returns struct <user>

Instead of specifying individual attributes in a create expression, we can pass a struct\<entity>.

create user(s); // s is struct<user>

Same rules as for the create expression apply: no need to specify attribute name if you can resolve it implicitly by name or type:

val name = 'Bob';
val address = 'New York';
val u = user(name, address);
val u2 = user(address, name); // Order does not matter - same struct value is created.

You can use . to access struct attributes:

print(u.name, u.address);

You can use safe-access operator ?. to read or modify attributes of a nullable struct:

val u: user? = find_user('Bob');
u?.balance += 100; // no-op if 'u' is null

Struct<mutable T>

struct<mutable T> has the same attributes as struct, but all attributes are mutable

To convert an entity or object to a mutable struct, one uses .to_mutable_struct()

val u = user @ { .name == 'Bob' };
val s = u.to_mutable_struct(); // will return a struct<mutable user>

To convert between struct<T> and struct<mutable T>, one instead uses .to_mutable() and .to_immutable()

val s = u.to_struct();
val mut = s.to_mutable();
val imm = mut.to_immutable();

struct<operation>

The type struct<operation> defines a struct which has same attributes as a given operations parameters:

operation add_user(name: text, rating: integer) {
// ...
}

query can_add_user(user: struct<add_user>) {
if (user.name == '') return false;
if (user.rating < 0) return false;
return true;
}

Enum

Enum declaration

enum currency {
USD,
EUR,
GBP
}

The values get stored in a database as integers. Each constant has a numeric value equal to its position in the enum (the first value is 0).

Usage

var c: currency;
c = currency.USD;

Enum-specific functions and properties:

val cs: list<currency> = currency.values() // Returns all values (in the order in which they are declared)

val eur = currency.value('EUR') // Finds enum value by name
val gbp = currency.value(2) // Finds enum value by index

val usd_str: text = currency.USD.name // Returns 'USD'
val usd_value: integer = currency.USD.value // Returns 0.

Query

  • Can't modify the data in the database (compile-time verification).
  • Must return a value.
  • If return type isn't explicitly specified, it's implicitly deducted.
  • Parameter types and return type must be GTV-compatible.

Short form

query q(x: integer): integer = x * x;

Full form

query q(x: integer): integer {
return x * x;
}

Operation

  • Can modify the data in the database.
  • Doesn't return a value.
  • Parameter types must be Gtv-compatible.
operation create_user(name: text) {
create user(name = name);
}

Function

  • Can return nothing or a value.
  • Can modify the data in the database when called from an operation (run-time verification).
  • Can get called from queries, operations or functions.
  • If return type isn't specified explicitly, it's unit (no return value).

Short form

function f(x: integer): integer = x * x;

Full form

function f(x: integer): integer {
return x * x;
}

Return type not specified

When return type isn't specified, it's considered unit:

function f(x: integer) {
print(x);
}

Default parameter values

Parameters of functions can have default values. If you don't specify the parameters in the function call, the default parameters get used.

function f(user: text = 'Bob', score: integer = 123) {...}
...
f(); // means f('Bob', 123)
f('Alice'); // means f('Alice', 123)
f(score=456); // means f('Bob', 456)

Named function arguments

One could also specify function arguments by names.

function f(x: integer, y: text) {}
...
f(x = 123, y = 'Hello');

Using a function as a value

If you want to pass a function to another function, then you can use the function as a value by using the following syntax:

() -> boolean
(integer) -> text
(byte_array, decimal) -> integer

Within the parentheses, you specify the function input type of the passed function. After the arrow follows the function's return type.

An example could look like this:

function filter(values: list<integer>, predicate: (integer) -> boolean): list<integer> {
return values @* { predicate($) };
}

Partial function app

You can use he wildcard symbol * to create a reference to a function (to obtain a value of a function).

function f(x: integer, y: integer) = x * y;

val g = f(*); // Type of "g" is (integer, integer) -> integer
g(123, 456); // Invocation of f(123, 456) via "g".

Extendable functions

You can declare a function as extendable by adding @extendable in front of the function declaration. You can define an arbitrary number of extensions for an extendable function by expressing @extend in front of the function declaration.

In the example below, function f is a base function and functions g and h are extension functions.

@extendable function f(x: integer) {
print('f', x);
}

@extend(f) function g(x: integer) {
print('g', x);
}

@extend(f) function h(x: integer) {
print('h', x);
}

When you call the base function, all its extension functions get executed, and the base function itself gets executed in the end. However, Extendable functions support a limited set of return types and this behavior depends on the return type. The following behavior applies to the different return types:

Unit

  • all extensions get executed
  • base function is always executed in the end

Boolean

  • extensions get executed one by one, until some of them returns "true"
  • base function gets executed if all extensions returned "false"
  • the result of the last executed function gets returned to the caller

T?

  • Similar to boolean. Extensions execute until the first non-null result, which gets returned to the caller

list<T>

  • all extensions get executed
  • the base function gets executed in the end
  • the concatenation of all lists get returned to the caller

map<K, V>

  • similar to list<T>, the union of all maps gets returned to the caller, but fails if there is a key conflict

Namespace

You can put definitions in a namespace:

namespace foo {
entity user {
name;
country;
}

struct point {
x: integer;
y: integer;
}

enum country {
USA,
DE,
FR
}
}

query get_users_by_country(c: foo.country) = foo.user @* { .country == c };

Features of namespaces:

  • No need to specify a full name within a namespace, can use country under namespace foo directly, not as foo.country.
  • Names of tables for entities and objects defined in a namespace contain the full name, for example, the table for entity foo.user is named c0.foo.user.
  • It's allowed to define namespace with same name multiple times with different inner definitions.

Anonymous namespace:

namespace {
// some definitions
}

You can use it to apply an annotation to a set of definitions:

@mount('foo.bar')
namespace {
entity user {}
entity company {}
}

Short nested namespace notation:

namespace x.y.z {
function f() = 123;
}

Is equivalent to:

namespace x {
namespace y {
namespace z {
function f() = 123;
}
}
}

Splitting namespace between files

You can split a namespace between different files in a module like:

lib/a.rell:

namespace ns { function f(): integer = 123; }

lib/b.rell:

namespace ns { function g(): integer = 456; }

which you can later access like:

main.rell:

import lib;
// ...
lib.f();
lib.g();

External

You can use the @external annotation to access entities defined in other blockchains.

@external('foo') namespace {
@log entity user {}
@log entity company {}
}

@external('foo') @log entity city {}

query get_all_users() = user @* {};

In this example, 'foo' is the name of an external blockchain. Before it's used in an @external annotation, you need to define the blockchain in the blockchain configuration (dependencies node).

Every blockchain has its chain_id, that's included in table names for entities and objects of that chain. If the blockchain 'foo' has chain_id = 123, the table for the entity user is c123.user.

Features

  • External entities must have the @log annotation. This implies that those entities can't have mutable attributes.
  • You can't create or delete the values of external entities.
  • You can annotate only entities, namespaces and imports with @externalannotation.
  • When you select the values of an external entity (using at-expression), an implicit block height filter gets applied, so the active blockchain can see only those blocks of the external blockchain whose height is lower than a specific value.
  • Every blockchain stores the structure of its entities in meta-information tables. When you start a blockchain, the meta-information of all involved external blockchains get verified to ensure that all declared external entities exist and have declared attributes.

External modules

You can annotate a module as @external with no arguments:

@external module;

@log entity user {}
@log entity company {}

Features

  • External modules can contain only namespaces, entities (annotated with @log) and imports of other external modules.
  • External modules can import as a regular or an external module.

Regular import: entities defined in the module ext belong to the current blockchain.

import ext;

External import: entities defined in the module ext get imported as external entities from the chain foo.

@external('foo') import ext;

Transactions and blocks

To access blocks and transactions of an external blockchain, you use the following syntax:

@external('foo') namespace foo {
entity transaction;
entity block;
}

function get_foo_transactions(): list<foo.transaction> = foo.transaction @* {};
function get_foo_blocks(): list<foo.block> = foo.block @* {};
  • External and non-external transactions/blocks are distinct, incompatible types.
  • When selecting external transactions or blocks, you use an implicit height filter (like for external entities).

You can access entities transaction and block of an external chain by an external module:

@external('foo') import ext;

function get_foo_transactions(): list<ext.transaction> = ext.transaction @* {};
function get_foo_blocks(): list<ext.block> = ext.block @* {};

The entities are implicitly added to the module's namespace and you can ceess them by their import alias.

Mount names

Entities, objects, operations, and queries have mount names:

  • for entities and objects, those names are the SQL table names where the data gets stored
  • for operations and queries,you use a mount name to invoke an operation or a query from the outside

By default, you define a mount name by a fully qualified name of a definition:

namespace foo {
namespace bar {
entity user {}
}
}

The mount name for the entity user is foo.bar.user.

Custom mount names

You can use the @mount annotation to specify a custom mount name:

@mount('foo.bar.user')
entity user {}

You can specify the @mount annotation can for entities, objects, operations and queries.

Mount for namespace

@mount('foo.bar')
namespace ns {
entity user {}
}

The mount name of user is foo.bar.user.

Mount for module

@mount('foo.bar')
module;

entity user {}

The mount name of user is foo.bar.user.

Nested namespace mounts

A mount name can be relative to the context mount name. For example, when defined in a namespace:

@mount('a.b.c')
namespace ns {
entity user {}
}

The entity user has the following mount names when annotated with @mount:

  • @mount('.d.user') -> a.b.c.d.user
  • @mount('^.user') -> a.b.user
  • @mount('^^.x.user') -> a.x.user

Special character . appends names to the context mount name, and ^ removes the last part from the context mount name.

A mount name can end with .. If that;s then case, then the name of the definition gets appended to the mount name:

@mount('foo.')
entity user {} // mount name = "foo.user"

@mount('foo')
entity user {} // mount name = "foo"