Module definitions¶
Table of Contents
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 typetransaction
is added to the entity. - When an entity value is created,
transaction
is set to the result ofop_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 namespacefoo
directly, not asfoo.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 namedc0.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"