Skip to content

Commit 0fa5700

Browse files
committed
docs: add query training guide and custom query example
Adds detailed documentation for the Query class and a custom query example. Fixes #910 Signed-off-by: Om7035 <[email protected]>
1 parent 46931e8 commit 0fa5700

File tree

3 files changed

+335
-1
lines changed

3 files changed

+335
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org).
55
This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
8-
98
### Added
9+
- Added `docs/sdk_developers/training/query.md` guide and `examples/query/custom_query_example.py` to demonstrate custom query implementation.
10+
- Added comprehensive documentation for the `Query` class architecture and usage.
1011

1112
- Add example demonstrating usage of `CustomFeeLimit` in `examples/transaction/custom_fee_limit.py`
1213
- Added `.github/workflows/merge-conflict-bot.yml` to automatically detect and notify users of merge conflicts in Pull Requests.
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# Understanding Queries in Hiero SDK
2+
3+
## Introduction
4+
5+
In the Hiero SDK, **Query** is a fundamental concept. It represents a request to the Hedera network to retrieve data without changing the network state. Unlike transactions, queries typically do not alter the ledger, although they may require a small payment to cover the cost of processing.
6+
7+
This guide explains how the `Query` class works, its relationship with `_Executable`, and how to implement your own custom queries.
8+
9+
## Contents
10+
11+
- [What is a Query?](#what-is-a-query)
12+
- [Architecture](#architecture)
13+
- [Inheritance Hierarchy](#inheritance-hierarchy)
14+
- [Key Concepts](#key-concepts)
15+
- [Execution Flow](#execution-flow)
16+
- [Query Payment](#query-payment)
17+
- [Automatic Cost Determination](#automatic-cost-determination)
18+
- [Retry Logic](#retry-logic)
19+
- [Building a Custom Query](#building-a-custom-query)
20+
- [Abstract Methods](#abstract-methods)
21+
- [Example Implementation](#example-implementation)
22+
- [Summary](#summary)
23+
24+
---
25+
26+
## What is a Query?
27+
28+
The `Query` class is the base class for all Hedera network queries. Queries allow you to request data from the Hedera network, such as:
29+
30+
- Account balances
31+
- Transaction records
32+
- Token information
33+
- Topic messages
34+
- File contents
35+
36+
Unlike transactions, queries do not change the network state. However, because processing a query consumes resources on the node, some queries require a payment in HBAR.
37+
38+
## Architecture
39+
40+
### Inheritance Hierarchy
41+
42+
All queries in the SDK inherit from the `Query` class, which in turn inherits from `_Executable`.
43+
44+
```
45+
_Executable
46+
└── Query
47+
├── AccountBalanceQuery
48+
├── TransactionRecordQuery
49+
├── TokenInfoQuery
50+
└── ... (other specific queries)
51+
```
52+
53+
This inheritance structure means that `Query` benefits from the unified execution engine provided by `_Executable`, including:
54+
55+
- **Automatic retries**: Handles temporary network failures.
56+
- **Node selection and rotation**: Automatically selects healthy nodes.
57+
- **gRPC network call orchestration**: Manages the underlying communication.
58+
- **Logging and error handling**: Provides consistent diagnostics.
59+
60+
### Key Concepts
61+
62+
`Query` acts as a bridge between the high-level user request and the low-level gRPC calls. It handles:
63+
64+
1. **Payment Management**: Ensuring the node is paid for the query.
65+
2. **Request Construction**: Building the Protobuf messages.
66+
3. **Response Parsing**: Converting Protobuf responses into SDK objects.
67+
68+
---
69+
70+
## Execution Flow
71+
72+
When you call `.execute(client)` on a query object, the following steps occur:
73+
74+
1. **Pre-execution setup (`_before_execute`)**:
75+
- Assigns nodes and an operator from the client.
76+
- Determines if payment is required.
77+
- If no payment is set, it may query the network for the cost.
78+
79+
2. **Request building (`_make_request`)**:
80+
- Constructs the Protobuf request for the specific query type.
81+
- Includes the payment transaction (if required) in the request header.
82+
83+
3. **gRPC call (`_get_method` + `_execute_method`)**:
84+
- Sends the request to the selected node using the appropriate gRPC method.
85+
- Handles retries and backoff strategies for temporary failures.
86+
87+
4. **Response mapping (`_map_response`)**:
88+
- Receives the raw Protobuf response.
89+
- Converts it into a usable SDK object (or extracts the relevant part).
90+
91+
5. **Retry handling (`_should_retry`)**:
92+
- Checks the response status code.
93+
- Automatically retries queries with statuses like `BUSY` or `PLATFORM_NOT_ACTIVE`.
94+
95+
6. **Error mapping (`_map_status_error`)**:
96+
- If the query fails (e.g., `INVALID_ACCOUNT_ID`), converts the status into a Python exception like `PrecheckError` or `ReceiptStatusError`.
97+
98+
---
99+
100+
## Query Payment
101+
102+
Some queries require a payment to the node. This payment is handled via a small `CryptoTransfer` transaction attached to the query request header.
103+
104+
### Setting Custom Payment
105+
106+
You can manually set the payment amount if you know the cost or want to offer a specific fee:
107+
108+
```python
109+
from hiero_sdk_python.hbar import Hbar
110+
111+
query = AccountBalanceQuery(account_id)
112+
query.set_query_payment(Hbar(1)) # Set custom payment to 1 Hbar
113+
```
114+
115+
### Automatic Cost Determination
116+
117+
If no payment is set, the SDK can automatically query the cost before executing the actual query.
118+
119+
1. The SDK sends a lightweight "Cost Query" (`COST_ANSWER` mode).
120+
2. The network returns the required fee.
121+
3. The SDK sets this fee as the payment and executes the actual query (`ANSWER_ONLY` mode).
122+
123+
You can also manually fetch the cost:
124+
125+
```python
126+
cost = query.get_cost(client)
127+
print(f"Query cost: {cost} Hbar")
128+
```
129+
130+
The SDK constructs a signed payment transaction using the operator’s private key to pay for the query.
131+
132+
---
133+
134+
## Retry Logic
135+
136+
Queries automatically handle retries for certain network issues. The base `_should_retry` implementation handles these statuses:
137+
138+
- `BUSY`: The node is busy.
139+
- `PLATFORM_TRANSACTION_NOT_CREATED`: The transaction wasn't created on the platform.
140+
- `PLATFORM_NOT_ACTIVE`: The platform is not active.
141+
142+
Subclasses can override `_should_retry` to add custom retry logic if needed.
143+
144+
---
145+
146+
## Building a Custom Query
147+
148+
If you need to implement a new type of query (e.g., for a new Hedera service), you can subclass `Query`.
149+
150+
### Abstract Methods
151+
152+
Subclasses **must** implement the following methods:
153+
154+
| Method | Purpose |
155+
| :--- | :--- |
156+
| `_make_request()` | Builds the Protobuf request for the specific query. |
157+
| `_get_query_response(response)` | Extracts the specific query response field from the full network response. |
158+
| `_get_method(channel)` | Returns the gRPC method wrapper (`_Method`) to call for this query. |
159+
160+
Optionally, you can override:
161+
162+
| Method | Purpose |
163+
| :--- | :--- |
164+
| `_map_response(response, node_id, proto_request)` | Customize how the response is returned to the user. |
165+
| `_should_retry(response)` | Customize retry logic. |
166+
| `_map_status_error(response)` | Customize error mapping. |
167+
168+
### Example Implementation
169+
170+
Here is an example of how to implement a simple `AccountBalanceQuery`:
171+
172+
```python
173+
from hiero_sdk_python.query.query import Query
174+
from hiero_sdk_python.executable import _Method
175+
from hiero_sdk_python.hapi.services import query_pb2, crypto_get_account_balance_pb2
176+
177+
class CustomAccountBalanceQuery(Query):
178+
def __init__(self, account_id):
179+
super().__init__()
180+
self.account_id = account_id
181+
182+
def _make_request(self):
183+
# Create the header (includes payment)
184+
header = self._make_request_header()
185+
186+
# Create the specific query body
187+
body = crypto_get_account_balance_pb2.CryptoGetAccountBalanceQuery(
188+
header=header,
189+
accountID=self.account_id._to_proto()
190+
)
191+
192+
# Wrap it in the main Query object
193+
return query_pb2.Query(cryptoGetAccountBalance=body)
194+
195+
def _get_query_response(self, response):
196+
# Extract the balance response from the main response
197+
return response.cryptoGetAccountBalance
198+
199+
def _get_method(self, channel):
200+
# Return the gRPC method to call
201+
return _Method(query_func=channel.crypto.get_account_balance)
202+
203+
# Usage
204+
# query = CustomAccountBalanceQuery(my_account_id)
205+
# balance = query.execute(client)
206+
```
207+
208+
---
209+
210+
## Summary
211+
212+
- **Query** handles all network-level logic; subclasses only implement query-specific behavior.
213+
- **Payment** is handled transparently, with support for automatic cost fetching.
214+
- **Reliability** is ensured through automatic retry and error handling.
215+
- **Extensibility** is achieved by subclassing `Query` and implementing the required abstract methods.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
Custom Query Example
3+
4+
This script demonstrates how to create a custom query by subclassing the base Query class.
5+
This is useful when you need to implement a query that isn't already supported by the SDK,
6+
or if you want to wrap an existing query with custom logic.
7+
8+
In this example, we re-implement a simple version of AccountBalanceQuery to show
9+
the internal mechanics of building a query.
10+
11+
Run with:
12+
python examples/query/custom_query_example.py
13+
"""
14+
import os
15+
import sys
16+
from dotenv import load_dotenv
17+
18+
from hiero_sdk_python import (
19+
Network,
20+
Client,
21+
AccountId,
22+
PrivateKey,
23+
Hbar,
24+
)
25+
from hiero_sdk_python.query.query import Query
26+
from hiero_sdk_python.executable import _Method
27+
from hiero_sdk_python.hapi.services import query_pb2, crypto_get_account_balance_pb2
28+
29+
load_dotenv()
30+
network_name = os.getenv('NETWORK', 'testnet').lower()
31+
32+
class CustomAccountBalanceQuery(Query):
33+
"""
34+
A custom implementation of AccountBalanceQuery to demonstrate subclassing Query.
35+
"""
36+
def __init__(self, account_id):
37+
super().__init__()
38+
self.account_id = account_id
39+
40+
def _make_request(self):
41+
"""
42+
Builds the Protobuf request for the query.
43+
"""
44+
# Create the header (includes payment logic)
45+
header = self._make_request_header()
46+
47+
# Create the specific query body
48+
# We use the raw protobuf classes here
49+
body = crypto_get_account_balance_pb2.CryptoGetAccountBalanceQuery(
50+
header=header,
51+
accountID=self.account_id._to_proto()
52+
)
53+
54+
# Wrap it in the main Query object
55+
return query_pb2.Query(cryptoGetAccountBalance=body)
56+
57+
def _get_query_response(self, response):
58+
"""
59+
Extracts the specific query response from the full network response.
60+
"""
61+
return response.cryptoGetAccountBalance
62+
63+
def _get_method(self, channel):
64+
"""
65+
Returns the gRPC method to call.
66+
"""
67+
return _Method(query_func=channel.crypto.get_account_balance)
68+
69+
def setup_client():
70+
"""
71+
Initialize and configure the Hiero SDK client.
72+
"""
73+
network = Network(network_name)
74+
client = Client(network)
75+
76+
operator_id_str = os.getenv('OPERATOR_ID')
77+
operator_key_str = os.getenv('OPERATOR_KEY')
78+
79+
if not operator_id_str or not operator_key_str:
80+
raise ValueError(
81+
"OPERATOR_ID and OPERATOR_KEY environment variables must be set")
82+
83+
operator_id = AccountId.from_string(operator_id_str)
84+
operator_key = PrivateKey.from_string(operator_key_str)
85+
client.set_operator(operator_id, operator_key)
86+
87+
return client, operator_id
88+
89+
def main():
90+
try:
91+
print("Setting up client...")
92+
client, operator_id = setup_client()
93+
print(f"Client setup with operator: {operator_id}")
94+
95+
# Create our custom query
96+
print(f"\nExecuting CustomAccountBalanceQuery for {operator_id}...")
97+
query = CustomAccountBalanceQuery(operator_id)
98+
99+
# Execute the query
100+
# This will trigger the full execution flow:
101+
# _before_execute -> _make_request -> gRPC call -> _map_response
102+
balance_response = query.execute(client)
103+
104+
# The response is the raw protobuf object because we didn't override _map_response
105+
# to convert it to a nice SDK object (like the official AccountBalanceQuery does).
106+
# So we access the protobuf fields directly.
107+
balance_tinybars = balance_response.balance
108+
balance_hbar = Hbar.from_tinybars(balance_tinybars)
109+
110+
print(f"✓ Balance retrieved successfully")
111+
print(f" Balance: {balance_hbar} ({balance_tinybars} tinybars)")
112+
113+
except Exception as e:
114+
print(f"✗ Error: {e}", file=sys.stderr)
115+
sys.exit(1)
116+
117+
if __name__ == "__main__":
118+
main()

0 commit comments

Comments
 (0)