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:
- Show the username of a user.
- Show how many followers a user has.
- Show how many users a user is following.
- Check if a user follows another user.
- Get a list of posts.
- 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:
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.
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
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
:
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:
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
, andpost
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 alimit
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 ofpost_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
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:
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:
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
, andCharlie
. Alice
followsBob
andCharlie
.- We use
assert_equals
to check if the queries return the correct counts forfollowers
andfollowing
.
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.
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
andBob
. Alice
followsBob
.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!