Tutorial

Language overview

Rell is a language for relational blockchain programming. It combines the following features:

  1. Relational data modeling and queries similar to SQL. People familiar with SQL should feel at home once they learn the new syntax.
  2. Normal programming constructs: variables, loops, functions, collections, etc.
  3. Constructs which specifically target application backends and, in particular, blockchain-style programming including request routing, authorization, etc.

Rell aims to make programming as ergonomic as possible. It minimizes boilerplate and repetition. At the same time, as a static type system it can detect and prevent many kinds of defects.

Blockchain programming

There are many different styles of blockchain programming. In the context of Rell, we see blockchain as a method for secure synchronization of databases on nodes of the system. Thus Rell is very database-centric.

Programming in Rell is pretty much identical to programming application backends: you need to handle requests to modify the data in the database and other requests which retrieve data from a database. Handling these two types of requests is basically all that a backend does.

But, of course, before you implement request handlers, you need to describe your data model first.

Class definitions

In SQL, usually you define your data model using CREATE TABLE syntax. In Java, you can define data objects using class definition. (See the section about languagespec-classes in the language specification for more information).

Rell uses persistent objects, thus a class definition automatically creates the storage (e.g. a table) necessary to persist objects of a class. As you might expect, Rell’s class definition includes a list of attributes:

class user {
    pubkey: pubkey;
    name: text;
    age: integer;
}

It is very common that the name of the attribute is the same as its type. For example, it makes sense to call user’s pubkey “pubkey.” Rell allows you to shorten pubkey: pubkey; to just pubkey;. Rell also has a number of convenient semantic types (see here: languagespec-types), so there is a type called name as well. Thus you can rewrite the definition above as just:

class user { pubkey; name; }

Typically a system should not allow different users to have the same name. That is, names should be unique. If name is unique, it can be used to identify a user. In Rell, this can be done by defining a key, i.e. key name;. Note that it’s not necessary to define both key and attribute. Rell is smart enough to figure out that if you use an attribute in a key, that attribute should exist in a class.

It also might be useful to find a user by his pubkey. Should it also be unique? Not necessarily. A user might have several different identities. When you want to enable fast retrieval, but do not need uniqueness, you can use index definition:

class user {
   key name;
   index pubkey;
}

However, if you want pubkey to be unique for an user, you can add a second key:

class user {
   key name;
   key pubkey;
}

(See languagespec-keys for more information.)

Typically, when you define a class in a programming language, it creates a type which can be used to refer to instances of that class. This is exactly how it works in Rell. The definition of class user creates a type user which is a type of references to objects stored in a database. References can themselves be used as attributes. For example, you might want to define something owned by a user, say, a channel. You can describe it like this:

class channel {
   index owner: user;
   key name;
}

index makes it possible to efficiently find all channels owned by a user. key makes sure that channel names are unique within the system.

Let’s analyze channel class definition from a point of view of a traditional relational database terminology. A single user can be associated with multiple channel objects, but a single channel is always related to a single user. Thus this represents one-to-many relationship. owner attribute of a channel refers to user object and thus constitutes a foreign key.

If channel names should be unique only in context of a single user (e.g. alice/news and bob/news are different channels), then a composite key can be used:

class channel {
   key owner: user, name;
}

This basically means that a pair of (owner, name) should be unique.

Finally, one might ask: what changes if we change index owner: user to key owner: user? This makes a user reference unique per channel table, thus there can be at most one channel per user in that case. (I.e. if owner is declared as a key, relationship between users and channels becomes a one-to-one relationship.)

Operations

Now that we defined the data model, we can finally get to handling requests. As previously mentioned, Rell works with two types of requests:

  1. Data-modifying requests. We call them operations which are applied to the database state.
  2. Data-retrieving requests. We call them queries.

But for both types of requests we are going to need to refer to things in the database, so let’s consider relational operators first.

Relational operator basics

First, let’s look how we create objects:

create user (pubkey=x"0373599a61cc6b3bc02a78c34313e1737ae9cfd56b9bb24360b437d469efdf3b15",
             name="Alice");

This is essentially the same as INSERT operation in SQL, but the syntax is a bit different. Rell is smart enough to identify the connection between arguments and attributes based on their type. x"..." notation is a hexadecimal byte_array literal which is compatible with pubkey type. On the other hand, name is provided via text literal. Thus we can write:

create user("Alice", x"0373599a61cc6b3bc02a78c34313e1737ae9cfd56b9bb24360b437d469efdf3b15");

The order of arguments does not matter here, they are matched with attributes based on types.

How do we find that object now?

val alice = user @ {.name=="Alice"};

@ operator retrieves a single record (or an object in this case) satisfying the search criteria you provided. If there is no such record, or more than one exists, it raises an error. It’s recommended to use this construct when an operation needs a single record to operate on. If this requirement is violated the operation will be aborted and all its effects will be rolled back. Thus it is a succinct and effective way to deal with requirements.

(val defines a read-only variable which can later be used in an expression. A variable defined using var can be reassigned later.)

If you want to retrieve a list of users, you can use the @* operator. For example:

val all_users = user @* {};

This returns a list of all users (since no filter expression was provided, all users match it). Value declarations can include a type, for example, we can specify that all_users is of type list<user> like this:

val all_users: list<user> = user @* {};

Since the Rell compiler knows a type of every expression it does not really need a type declaration, however, if one is provided, it will check against it. Type declarations are mostly useful as documentation for programmers reading the code and should be omitted in cases where there is no ambiguity.

Both @ and @* correspond to SELECT in SQL. A complete list of operators can be found in languagespec-operators.

Simple operation

Let’s make an operation which allows a user to create a new channel:

operation register_channel (user_pubkey: pubkey, channel_name: name) {
    require( is_signer(user_pubkey) );
    create channel (
        owner = user@{.pubkey == user_pubkey},
        name = channel_name
    );
}

Let’s go through this line by line. First we declare the operation name and a list of parameters:

operation register_channel (user_pubkey: pubkey, channel_name: name) {

This is very similar to a function definitions in other languages. In fact, an operation is a function of a special kind: it can be invoked using a blockchain transaction by its name. When invoking register_channel, the caller must provide two arguments of specified types, otherwise it will fail.

require( is_signer(user_pubkey) );

We don’t want Alice to be able to pull a prank on Bob by registering a channel with a silly name on his behalf. Thus we need to make sure that the transaction was signed with a key corresponding to the public key specified in the first parameter. (In other words, if Bob’s public key is passed as user_pubkey, the transaction must also be signed by Bob, that is, Bob is a signer of this transaction.) This is a common pattern in Rell – typically you specify an actor in a parameter of an operation and in the body of the operation you verify that the actor was actually the signer. require fails the operation if the specified condition is not met.

Note

What happens with a failed operation? A transaction which contains this operation will be rejected, that is, it won’t be included into a blockchain and will be eventually forgotten. When developer is debugging an application he might be able to retrieve a message which describes a reason why a transaction was rejected. However, in a real production use this cannot be relied upon, as blockchain nodes can be bombed with large quantities of invalid transactions, thus nodes normally won’t store a list of rejected transactions.

create channel, obviously, creates a persistent object channel. You don’t need to explicitly store it, as all created objects are persisted if operation succeeds.

user@{.pubkey=user_pubkey} – now we retrieve a user object by its pubkey, which should be unique. If no such user exists operation will fail. We do not need to test for that explicitly as @ operator will do this job.

Rell can automatically find attribute names corresponding to arguments using types. As user and name are different types, create channel can be written like this:

create channel (user@{.pubkey=user_pubkey}, channel_name);

Function

Sometimes multiple operations (or queries) need a same piece functionality, e.g. some kind of a validation code, or code which retrieves objects in a particular way. In order to not repeat yourself you can use function. Functions work similarly to operations: they get some input and can perform validations and work with data. Additionally, they also have a return type which can be specified after the list of parameters. For example, if you want to allow the user of a channel to change the name of the channel itself:

// We added mutable specifier to channel's attribute "name" to make name editable.
// Note that in case both an attribute and a key need to be declared.

class channel {
    mutable name;
    key name;
    index owner: user;
}

function get_channel_owned_by_user(user_pub: pubkey, channel_name: name): channel {
    val user = user@{.pubkey == user_pub};
    return channel@{channel_name, .owner == user};
}

operation change_channel_name(signer: pubkey, old_channel_name: name, new_channel_name: name) {
    require(is_signer(signer));
    val channel_to_change = get_channel_owned_by_user(signer, old_channel_name);
    update channel@{channel == channel_to_change}(.name = new_channel_name);
}

In the function get_channel_owned_by_user the code first retrieves a user with given public key and returns a channel owned by the the retrieved user with the given channel name. Operator @ expects exactly one object to be found (see Cardinality for more information.), thus you can be sure that in case there is no user or channel with such a pubkey or a name the function will fail and so will the operation that is calling it. Finally, the function returns the channel instance that was validated, saving the developer the hassle to check owner every time a channel is retrieved.

Please note that you must mark the attribute name with the keyword mutable. This is because only the fields which are declared mutable can be changed using the update statement.

Query

Storing data without the ability to access it again would be useless. Let’s consider a simple example - retrieving channel names for a user with a certain name:

::
query get_channel_names (user_name: name) {
return channel @* {
.owner == user@{.name==user_name}

} (.name);

}

Here you see a selection operator you’re already familiar with – @*. We select all the channels with a given owner (which we first find by name).

Then we extract name attribute from retrieved objects using the (.name) construct.

Note that since we only need name from channel, is also possible to write

::
query get_channel_names (user_name: name) {
return channel @* {
.owner == user@{.name==user_name}

}.name;

}

Relational expressions

In general, a relational expression consists of five parts, some of which can be omitted:

FROM OPERATOR { WHERE } (WHAT) LIMIT
  1. FROM describes where data is taken from. It can be a single class, such as just user. Or, it can be combination of multiple classes, e.g. (user, channel). In the later case, conceptually we are dealing with a Cartesian product, which is a set of all possible combinations. But, in typically WHERE part will then provide a condition which defines a correspondence between objects of difference classes. E.g. one can select such (user, channel) combinations where user is an owner of the channel. This works same way as JOIN in SQL, in fact, the optimizer will typically translate it to JOINs.
  2. OPERATOR – there are different operators depending on required cardinality. They are:
    • @ – exactly one, returns a value
    • @* – any number, returns a list of values
    • @+ – at least one, returns a list of values
    • @? – one or zero, returns a nullable value
  3. WHERE describes how to filter the FROM set. So, you would use your search criteria as well as JOINs.
  4. WHAT describes how to process the set, for doing a projection, aggregation or sorting. If it is ommitted then members of the set are returned as they are.
  5. LIMIT for operators which return a list, limits the number of elements returned.

In SQL, the logical processing order does not match the order in which clauses are written, for example, FROM is logically processed before SELECT even though SELECT comes first. (SQL logical processing order can be found e.g. in SQL Server documentation).

The order of components of a relational expression in Rell matches the logical processing order. So, first a set is defined, then it is filtered, and then it is post-processed. Of course, the query planner is allowed to perform operations in a different order, but that shouldn’t affect the results. Thus a relational expression can be understood as a kind of a pipeline.

Let’s see some examples of relational expressions. Suppose in addition to user and channel classes we provided before, we also have:

class message {
    index channel;
    index timestamp;
    text;
}

We can retrieve all messages of a given user:

(channel, message) @* {
    channel.owner == given_user, message.channel == channel
}(message.text);

So, basically, we join channel with message. We can shorten the expression using class aliases:

(c: channel, m: message) @* { c.owner == given_user, m.channel == c } (m.text, m.timestamp)

We can easily read this expression left to right:

  • consider all pairs (c, m) where c is channel and m is message
  • find those where c.owner equals given_user and m.channel equals c
  • extract text and timestamp from m

The result of this expression is a list of tuples with text and timestamp attributes.

The above expression can be easily modified to retrieve the latest 25 messages:

(c: channel, m: message) @* {
    c.owner == given_user, m.channel == c
} (m.text, -sort m.timestamp) limit 25

Here we sorted results by timestamp in a descending order using -sort (minus prefix means descending) and limited the number of returned rows.

Composite indices

We can also only select recent messages by adding, for example, m.timestamp >= given_timestamp condition to WHERE part. But a database cannot filter messages efficiently (that is, without considering every message) using two criteria at once unless we create a composite index, changing the message class definition in the following way:

class message {
    index channel, timestamp;
    text;
}

Instead two separate indexes we got one composite index. The idea here is that we want to retrieve not the latest messages overall, but the latest messages for a given channel. Thus, we need to order messages by channels first. Paged retieval can be done using the following query:

query get_next_messages (user_name: name, upto_timestamp: timestamp) {
   val given_user = user@{user_name};
   return (c: channel, m: message) @* {
       c.owner == given_user, m.channel == c, m.timestamp < upto_timestamp
   } (m.text, -sort m.timestamp) limit 25;
}

This can be used in an app like Twitter. A visitor might first retrieve the latest 25 messages, then go further – in which case the client will send a query with a timestamp of the oldest message retrieved.

To understand why this can work efficiently, consider that the index stores an ordered collection of pairs. For example:

1. (channel_1, 1000000) -> m1
2. (channel_1, 1000050) -> m3
3. (channel_1, 1000100) -> m5
4. (channel_2, 1000025) -> m2
5. (channel_2, 1000075) -> m4

A database can efficiently find a place which corresponds to a given timestamp in a given channel and traverse the index through it.

Note

It’s worth noting that all SQL databases work this way, this feature is not unique to Rell. But in a decentralized system resources are typically precious, thus it is important for Rell programmers to understand the query behavior and use indices efficiently.

Examples and further exercises

We have prepared some examples of how to implement other functionality in Rell and Chromia.