Skip to main content

Core concepts

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 that specifically target app 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 identical to programming app 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.

Entity definitions

In Rell, entities are used to define persistent objects with attributes. Entity definitions automatically create the storage necessary for object persistence. Attributes within entities are defined using simple syntax, often aligned with their data type. Keys and indexes can be specified for efficient retrieval, allowing unique identification or quick access to data.

For example,

ActionDescription
Define basic entityentity user { pubkey; name; } - create an entity user with attributes pubkey and name.
Add unique identifier (key)entity user { key name; } - define a unique key based on the name attribute for the user entity.
Create index for efficient retrievalentity user { key name; index pubkey; } - add an index for the pubkey attribute to enable fast retrieval in the user entity.
Ensure uniqueness with multiple keysentity user { key name; key pubkey; } - define both name and pubkey as keys to ensure uniqueness within the user entity.
Establish one-to-many relationshipentity channel { index owner: user; key name; } - create an entity channel with an index on owner and a key based on name.
Create composite key for contextual uniquenessentity channel { key owner: user, name; } - define a composite key (owner and name) for ensuring uniqueness within a user's context.
Make user reference unique per channel tableentity channel { key owner: user; key name; } - use separate keys (owner and name) to enforce unique references in the channel entity.

Managing data requests

In Rell, we handle requests through two types of actions:

  1. Data-modifying requests (Operations): These are operations that modify the database state.
  2. Data-retrieving requests (Queries): These are requests for retrieving data.

When dealing with both types of requests, relational operators play a crucial role.

Operations

To create objects, use the create operation:

create user (pubkey=x"...", name="Alice");
  • x"..." is a hexadecimal byte_array literal for the pubkey.
  • The order of arguments is flexible, matched with attributes based on types.

Queries

Data storage is only valuable if you can access the stored information. For example, consider the task of retrieving channel names for a user with a specific name:

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

Here, we use the @* operator to select all channels owned by a user found by name. We then extract the name attribute from the retrieved objects using (.name).

A more concise alternative is:

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

For more information see, At-operator.

Functions

In Rell, functions streamline repetitive tasks by encapsulating reusable logic, such as validation or data retrieval. Similar to operations, functions take inputs, perform actions, and return values.

Example scenario:

entity 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(op_context.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 get_channel_owned_by_user, a user is retrieved based on public key, and a channel owned by that user with a specific name is returned. The @ operator expects a single object, ensuring validation. The function simplifies subsequent logic, enhancing efficiency and accuracy.

Note that attributes, like name, must be marked mutable for updates using the update statement. For more information see, Function.

Relational expressions

In Rell, relational expressions form the core of data retrieval and manipulation. Consisting of five elements, these expressions enable efficient querying:

| FROM OPERATOR { WHERE } (WHAT) LIMIT

  1. FROM: Specifies data sources, potentially joined and filtered.
  2. OPERATOR: Determines result cardinality: @, @*, @+, @?.
  3. WHERE: Filters data based on conditions and joins.
  4. WHAT: Defines projection, aggregation, and sorting.
  5. LIMIT: Restricts the number of returned elements.

Rell's logical processing follows this sequence, simplifying data handling. Unlike SQL, where processing order can vary, Rell maintains coherence.

Example expression:

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

Here:

  • Pairs (c, m) are selected from channel and message.
  • Data is filtered by owner and channel correspondence.
  • text is projected and sorted by timestamp in descending order.
  • Results are limited to 25 rows.

Composite indices

To efficiently filter data using multiple criteria, we utilize composite indices. To achieve this, we modify the entity definition with a composite index:

entity message {
index channel, timestamp;
text;
}

This composite index orders messages by channels, enabling targeted retrieval. Consider the following query for paged retrieval:

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 query is suited for applications like Twitter, allowing you to progressively access messages by timestamp.

The composite index's ordered collection of pairs facilitates efficient retrieval. 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

By leveraging this index, the database efficiently locates a timestamp within a channel, ensuring streamlined traversal.

note

While common to SQL databases, this approach is vital in resource-constrained decentralized systems like Rell. As a developer, you should comprehend query behavior and index utilization for optimal results.