Module definitions


Entity

Values (instances) of an entity in Rell are stored in a database, not in memory. They have to be created and deleted explicitly using 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;
}

If attribute type is not specified, it will be the same as attribute name:

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';
}

An ID (database primary key) of an entity value can be accessed via the rowid implicit attribute (of type rowid):

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;
}

Mutability can be specified within a key or index clause. Here one can also set a default value:

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

Attribute definitions can be combined with key or index clauses, but such definition has restrictions (e. g. cannot 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:

  • Special attribute transaction of type transaction is added to the entity.
  • When an entity value is created, transaction is set to the result of op_context.transaction (current transaction).
  • Entity cannot have mutable attributes.
  • Values cannot be deleted.

Changing existing entities

When starting a Rell app, database structure update is performed: tables for new entities and objects are created and tables for existing ones are altered. There are limitations on changes that can be made in existing entities and objects.

What’s allowed:

  • Adding an attribute with a default value (a column is 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 is not dropped, and the attribute can be readded 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 are stored in a database.
  • Objects are initialized automatically during blockchain initialization.
  • Cannot 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 may have a default value, which is used if the attribute is not specified during construction.
  • Structs are deleted from memory implicitly by a garbage collector.

Creating struct values:

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

A struct-copy of an entity or object can also be created with .to_struct()

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 it can be resolved 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.

Struct attributes can be accessed using operator .:

print(u.name, u.address);

Safe-access operator ?. can be used 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
}

Values are 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

  • Cannot modify the data in the database (compile-time check).
  • Must return a value.
  • If return type is not explicitly specified, it is 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.
  • Does not 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 check).
  • Can be called from queries, operations or functions.
  • If return type is not specified explicitly, it is unit (no return value).

Short form:

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

Full form:

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

When return type is not specified, it is considered unit:

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

Default parameter values

Parameters of functions can have default values. If no parameters are specified in the function call, the default parameters will be 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 one would want to pass a function to another function, the function to be passed can be used as a value by using the following syntax:

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

Within the parentheses, the function input type(s) of the passed function is specified. 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 application

If one would want to create a reference to a function i. e. to obtain a value of a function, the wildcard symbol * could be used.

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

A function can be declared as extendable by adding @extendable in front of the function declaration. An arbitrary number of extensions can be defined 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 the base function is called, all its extension functions are executed, and the base function itself is 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 are executed
    • base function is always executed in the end
  • boolean
    • extensions are executed one by one, until some of them returns “true”
    • base function is executed if all extensions returned “false”
    • the result of the last executed function is returned to the caller
  • T?
    • Similar to boolean. Extensions are executed until the first non-null result, which is returned to the caller
  • list<T>
    • all extensions are executed
    • the base function is executed in the end
    • the concatenation of all lists is returned to the caller
  • map<K, V>
    • similar to list<T>, the union of all maps is returned to the caller, but fails if there is a key conflict

Namespace

Definitions can be put 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, i.e. 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, e. g. the table for entity foo.user will be named c0.foo.user.
  • It is allowed to define namespace with same name multiple times with different inner definitions.

Anonymous namespace:

namespace {
    // some definitions
}

Can be used 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

One can split a namespace between different files in a module like so:

lib/a.rell:

    namespace ns { function f(): integer = 123; }
lib/b.rell:

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

Which later can be accessed like so:

main.rell:

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

External

The @external annotation is used 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. To be used in an @external annotation, a blockchain must be defined in the blockchain configuration (dependencies node).

Every blockchain has its chain_id, which is 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 will be called c123.user.

Features:

  • External entities must be annotated with the @log annotation. This implies that those entities cannot have mutable attributes.
  • Values of external entities cannot be created or deleted.
  • Only entities, namespaces and imports can be annotated with @external.
  • When selecting values of an external entity (using at-expression), an implicit block height filter is 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 a blockchain is started, the meta-information of all involved external blockchains is verified to make sure that all declared external entities exist and have declared attributes.

External modules

A module can be annotated with the @external with no arguments:

@external module;

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

An external module:

  • can contain only namespaces, entities (annotated with @log) and imports of other external modules;
  • can be imported 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 are imported as external entities from the chain foo.

@external('foo') import ext;

Transactions and blocks

To access blocks and transactions of an external blockchian, a special syntax is used:

 @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, an implicit height filter is applied (like for external entities).

Entities transaction and block of an external chain can be accessed also via 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 can be accessed by its 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 is stored
  • for operations and queries, a mount name is used to invoke an operation or a query from the outside

By default, a mount name is defined 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.

To specify a custom mount name, @mount annotation is used:

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

The @mount annotation can be specified for entities, objects, operations and queries.

In addition, it can be specified for a namespace:

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

or a module:

@mount('foo.bar')
module;

entity user {}

In both cases, the mount name of user is foo.bar.user.

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 {}
}

entity user will have 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 ., in that case the name of the definition is appended to the mount name:

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

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