Skip to main content

Write basic queries

In this section, we'll introduce you to the basics of working with queries in Chromia blockchain development. Queries are essential for retrieving data from the blockchain, and we'll show you how to create and test them step by step.

In our dapp, we need queries to:

  1. Show the username of a user.
  2. Show how many followers a user has.
  3. Show how many users a user is following.
  4. Check if a user follows another user.
  5. Get a list of posts.
  6. Show all users.

Here's how to create and use queries in Chromia:

User and follower queries

Queries are defined as functions with the query keyword. We'll start by creating a query to get a uniquely identifiable username:

src/main.rell
query get_user_name(user_id: byte_array): text {
return user @ { user_id } ("%s#%s".format(.name, .id.to_hex().sub(0, 5)));
}

This query finds a user and formats a text by concatenating the name and the first five letters of the hex representation of the ID. Next, we'll create two queries to count followers and those a user follows.

src/main.rell
query get_followers_count(user_id: byte_array): integer {
return follower @ { .user == user @ { user_id } } (@sum 1);
}

query get_following_count(user_id: byte_array): integer {
return follower @ { .follower == user @ { user_id } } (@sum 1);
}

The queries are quite similar but differ in filtering the results from the follower database query. The return type integer could be omitted since Rell can deduce this from the return statement, and the query fits on a single line. We could also omit the braces and the return statement as follows:

query get_followers_count(user_id: byte_array) =
follower @ { .user == user @ { user_id } } (@sum 1);

We also create a query that checks if a follower entity exists

src/main.rell
query is_following(my_id: byte_array, your_id: byte_array) =
exists(follower @? { .user.id == your_id, .follower.id == my_id });

Query posts with pagination

Now, we want to create a query to get posts created by users a user follows. This might result in many posts, so we need pagination to manage the data effectively. The results should be ordered from the latest to the oldest posts.

To handle post data efficiently, we'll define a struct called post_dto:

src/main.rell
struct post_dto {
timestamp;
user: struct<user>;
content: text;
}

This structure is similar to a post entity but the user field looks a bit different. The type struct<T> is an in-memory representation of an entity. This means that all fields are loaded into memory and can be efficiently used.

Retrieve posts

To get the desired posts, we'll need to join tables, filter data, and add pagination:

src/main.rell
query get_posts(
user_id: byte_array,
pointer: integer,
n_posts: integer
): (pointer: integer, posts: list<post_dto>) {
val posts = (user, follower, post) @* {
user.id == user_id,
follower.follower == user,
post.user == follower.user
} (
@sort_desc @omit post.rowid,
post_dto(
post.timestamp,
post.content,
user = post.user.to_struct()
)
) offset pointer limit n_posts;

return (
pointer = pointer + posts.size(),
posts = posts
);
}

Here's what's happening in this query:

  • We specify the user whose followers' posts we want to retrieve using user_id.
  • We join the user, follower, and post tables to obtain the necessary data.
  • Posts are sorted in descending order based on their creation timestamp to get the latest posts first.
  • We create a post_dto data structure for each post, including the user's structured representation.
  • To enable pagination, we include an offset to skip posts and a limit to control the number of posts to retrieve.

In the what part of the database query, we sort the posts in descending order to get the latest posts first but omit this from the resulting data structure. An alternate way would be to paginate based on timestamps, but we stick to this method for simplicity.

Return results

Finally, we return the results as a named tuple with two components:

  • pointer: An index indicating where to start the next query.
  • posts: A list of post_dto objects containing the retrieved posts.

This is an excellent way to create simple data structures without explicitly defining dto structs.

With this query, you can easily fetch posts created by a user's followers with pagination, simplifying managing and displaying the data in your dapp.

Query user list

Finally, we need a query to retrieve all users in the dapp. Combining parts from the get_user_name query with the pagination from get_posts, we can create this simple query

src/main.rell
query get_users(pointer: integer, n_users: integer) {
val users = user @* {} (name = "%s#%s".format(.name, .id.to_hex().sub(0, 5)), id = .id) offset pointer limit n_users;
return (
pointer = pointer + users.size(),
users = users
);
}

Test the queries

Now, let's test these queries to ensure they work as expected. We start by adding a small test case for the get_user_name query:

src/test/my_news_feed_test.rell
function test_user_name() {
rell.test.tx()
.op(create_user("Alice", alice))
.run();
assert_equals(get_user_name(alice), "Alice#02466");
val users_result = get_users(0, 20);
assert_equals(users_result.pointer, 1);
assert_equals(users_result.users.size(), 1);
assert_true(users_result.users @* {} (.name).contains("Alice#02466"));
}

As seen here, the @-operator works also for lists.

Then, we test the computation of the follower count by adding another test case:

src/test/my_news_feed_test.rell
val charlie = rell.test.pubkeys.charlie;

function test_follower_calculation() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.op(create_user("Charlie", charlie))
.run();

rell.test.tx()
.op(follow_user(alice, bob))
.op(follow_user(alice, charlie))
.run();

assert_true(is_following(alice, bob));
assert_true(is_following(alice, charlie));
assert_equals(get_following_count(alice), 2);
assert_equals(get_following_count(bob), 0);

assert_equals(get_followers_count(alice), 0);
assert_equals(get_followers_count(bob), 1);
}

In this test case:

  • We created three users: Alice, Bob, and Charlie.
  • Alice follows Bob and Charlie.
  • We use assert_equals to check if the queries return the correct counts for followers and following.

Test pagination for posts

Now, let's test the query for retrieving posts with pagination. We'll create a test case that involves creating users, making them follow each other, and then creating posts.

src/test/my_news_feed_test.rell
function test_pagination_of_posts() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.run();
rell.test.tx().op(follow_user(alice, bob)).run();

for (i in range(5)) {
rell.test.tx().op(make_post(bob, "Content %d".format(i))).run();
}

val initial_posts = get_posts(alice, 0, 4);
assert_equals(initial_posts.pointer, 4);
assert_equals(initial_posts.posts.size(), 4);
val last_posts = get_posts(alice, initial_posts.pointer, 4);
assert_equals(last_posts.pointer, 5);
assert_equals(last_posts.posts.size(), 1);
}

Here's what happens in this test case:

  • We create users Alice and Bob.
  • Alice follows Bob.
  • Bob creates five posts.
  • We use pagination to retrieve posts and check if the results are as expected.

Manually test the dapp

Beyond automated tests, you can also manually test your dapp by running a local test node and interacting with it using Chromia CLI.

Starting a test node

To start a local test node, run the following command from your project folder:

chr node start

You'll see logs as the node progresses to build blocks.

Create users and transactions

To create users, you need to use the public keys generated by calling chr keygen. In this tutorial, we'll use public keys for Alice and Bob:

  • Alice: 03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114
  • Bob: 0389F8109AF5D6670E96B49C6FE5FE2E62D793D948D6FB9138DEFD2A13C3B351D9

To create Alice as a new user, use the following transaction command:

chr tx --await create_user Alice 'x"03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114"'

transaction with rid TxRid(rid=9042157A8C2DC4B8C974510540067C7367DEE1AEC7FCE9BBF5FB6D6E0DC37F3C) was posted CONFIRMED

The --await flag ensures the transaction is confirmed in a block before completion.

Now, let's create Bob as a user.

chr tx --await create_user Bob 'x"0389F8109AF5D6670E96B49C6FE5FE2E62D793D948D6FB9138DEFD2A13C3B351D9"'

transaction with rid TxRid(rid=D74C829485EB1BB0A5DB39374067C2B5B53A92CF072D76B25624DD01908CC300) was posted CONFIRMED

We then make Alice follow Bob and create a post for Bob.

chr tx --await follow_user 'x"03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114"' 'x"0389F8109AF5D6670E96B49C6FE5FE2E62D793D948D6FB9138DEFD2A13C3B351D9"'

transaction with rid TxRid(rid=F5BC2D5C9979EFE0E91CF39A605DCD4E36235CB98CBE33EB661E595C99DF7A63) was posted CONFIRMED

chr tx --await make_post 'x"0389F8109AF5D6670E96B49C6FE5FE2E62D793D948D6FB9138DEFD2A13C3B351D9"' "My first post"

transaction with rid TxRid(rid=5686A5AD5FD2E15CAA29EF2BA1C687BD725ADABEAE323296374F212685A9AEA3) was posted CONFIRMED

Test queries

You can now test your queries manually using Chromia CLI. For example:

chr query get_following_count 'user_id=x"03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114"'

1

chr query get_followers_count 'user_id=x"03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114"'

0

chr query get_posts user_id='x"03854EAE78096078DB97B18E8900DA5518613F5460F3D49C1F52B0223CBB9BC114"' pointer=0 n_posts=10
{
"pointer": 1,
"posts": [
{
"content": "My first post",
"timestamp": 1694073942456,
"user": {
"name": "Bob",
"id": "x\"0389F8109AF5D6670E96B49C6FE5FE2E62D793D948D6FB9138DEFD2A13C3B351D9\""
}
}
]
}

This query returns the number of users Alice follows and fetches the post that Alice created for Bob.

Congratulations! You've completed the first module of this course. In the next module, we'll dive deeper into verifying user input in operations and securing your transactions against impersonation. Stay tuned!