Skip to content

Commit ba93bb8

Browse files
author
Matthias Zimmermann
committed
feat: add query language tests, some reamde extensions
1 parent 32780b2 commit ba93bb8

File tree

4 files changed

+540
-33
lines changed

4 files changed

+540
-33
lines changed

README.md

Lines changed: 169 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ entity_key, tx_hash = client.arkiv.create_entity(
115115

116116
entity = client.arkiv.get_entity(entity_key)
117117
exists = client.arkiv.exists(entity_key)
118-
```
118+
```
119119

120120
## Advanced Features
121121

@@ -126,9 +126,7 @@ The snippet below demonstrates the creation of various nodes to connect to using
126126
```python
127127
from arkiv import Arkiv
128128
from arkiv.account import NamedAccount
129-
from arkiv.provider import ProviderBuilder
130-
131-
## Advanced Features
129+
from arkiv.provider import ProviderBuilder## Advanced Features
132130

133131
### Provider Builder
134132

@@ -179,6 +177,173 @@ entities = list(client.arkiv.query_entities(query=query, options=options))
179177
print(f"Found {len(entities)} entities")
180178
```
181179

180+
### Query Language
181+
182+
Arkiv uses a SQL-like query language to filter and retrieve entities based on their attributes. The query language supports standard comparison operators, logical operators, and parentheses for complex conditions.
183+
184+
#### Supported Operators
185+
186+
**Comparison Operators:**
187+
- `=` - Equal to
188+
- `!=` - Not equal to
189+
- `>` - Greater than
190+
- `>=` - Greater than or equal to
191+
- `<` - Less than
192+
- `<=` - Less than or equal to
193+
194+
**Logical Operators:**
195+
- `AND` - Logical AND
196+
- `OR` - Logical OR
197+
- `NOT` - Logical NOT (can also use `!=`)
198+
199+
**Parentheses** can be used to group conditions and control evaluation order.
200+
201+
#### Query Examples
202+
203+
```python
204+
from arkiv import Arkiv
205+
206+
client = Arkiv()
207+
208+
# Simple equality
209+
query = 'type = "user"'
210+
entities = list(client.arkiv.query_entities(query))
211+
212+
# Note that inn the examples below the call to query_entities is omitted
213+
214+
# Multiple conditions with AND
215+
query = 'type = "user" AND status = "active"'
216+
217+
# OR conditions with parentheses
218+
query = 'type = "user" AND (status = "active" OR status = "pending")'
219+
220+
# Comparison operators
221+
query = 'type = "user" AND age >= 18 AND age < 65'
222+
223+
# NOT conditions
224+
query = 'type = "user" AND status != "deleted"'
225+
226+
# Alternative NOT syntax
227+
query = 'type = "user" AND NOT (status = "deleted")'
228+
229+
# Complex nested conditions
230+
query = '(type = "user" OR type = "admin") AND (age >= 18 AND age <= 65)'
231+
232+
# Multiple NOT conditions
233+
query = 'type = "user" AND status != "deleted" AND status != "banned"'
234+
```
235+
236+
**Note:** String values in queries must be enclosed in double quotes (`"`). Numeric values do not require quotes.
237+
238+
### Watch Entity Events
239+
240+
Arkiv provides near real-time event monitoring for entity lifecycle changes. You can watch for entity creation, updates, extensions, deletions, and ownership changes using callback-based event filters.
241+
242+
#### Available Event Types
243+
244+
- **`watch_entity_created`** - Monitor when new entities are created
245+
- **`watch_entity_updated`** - Monitor when entities are updated
246+
- **`watch_entity_extended`** - Monitor when entity lifetimes are extended
247+
- **`watch_entity_deleted`** - Monitor when entities are deleted
248+
- **`watch_owner_changed`** - Monitor when entity ownership changes
249+
250+
#### Basic Usage
251+
252+
```python
253+
from arkiv import Arkiv
254+
255+
client = Arkiv()
256+
257+
# Define callback function to handle events
258+
def on_entity_created(event, tx_hash):
259+
print(f"New entity created: {event.key}")
260+
print(f"Owner: {event.owner}")
261+
print(f"Transaction: {tx_hash}")
262+
263+
# Start watching for entity creation events
264+
event_filter = client.arkiv.watch_entity_created(on_entity_created)
265+
266+
# Create an entity - callback will be triggered
267+
entity_key, _ = client.arkiv.create_entity(
268+
payload=b"Hello World",
269+
attributes={"type": "greeting"}
270+
)
271+
272+
# Stop watching when done
273+
event_filter.stop()
274+
event_filter.uninstall()
275+
```
276+
277+
#### Watching Multiple Event Types
278+
279+
```python
280+
created_events = []
281+
updated_events = []
282+
deleted_events = []
283+
284+
def on_created(event, tx_hash):
285+
created_events.append((event, tx_hash))
286+
287+
def on_updated(event, tx_hash):
288+
updated_events.append((event, tx_hash))
289+
290+
def on_deleted(event, tx_hash):
291+
deleted_events.append((event, tx_hash))
292+
293+
# Watch multiple event types simultaneously
294+
filter_created = client.arkiv.watch_entity_created(on_created)
295+
filter_updated = client.arkiv.watch_entity_updated(on_updated)
296+
filter_deleted = client.arkiv.watch_entity_deleted(on_deleted)
297+
298+
# Perform operations...
299+
# Events are captured in real-time
300+
301+
# Cleanup all filters
302+
filter_created.uninstall()
303+
filter_updated.uninstall()
304+
filter_deleted.uninstall()
305+
```
306+
307+
#### Historical Events
308+
309+
By default, watchers only capture new events from the current block forward. You can also watch from a specific historical block:
310+
311+
```python
312+
# Watch from a specific block number
313+
event_filter = client.arkiv.watch_entity_created(
314+
on_entity_created,
315+
from_block=1000
316+
)
317+
318+
# Watch from the beginning of the chain
319+
event_filter = client.arkiv.watch_entity_created(
320+
on_entity_created,
321+
from_block=0
322+
)
323+
```
324+
325+
#### Automatic Cleanup
326+
327+
When using Arkiv as a context manager, all event filters are automatically cleaned up on exit:
328+
329+
```python
330+
with Arkiv() as client:
331+
# Create event filters
332+
filter1 = client.arkiv.watch_entity_created(callback1)
333+
filter2 = client.arkiv.watch_entity_updated(callback2)
334+
335+
# Perform operations...
336+
# Filters are automatically stopped and uninstalled when exiting context
337+
```
338+
339+
You can also manually clean up all active filters:
340+
341+
```python
342+
client.arkiv.cleanup_filters()
343+
```
344+
345+
**Note:** Event watching requires polling the node for new events. The SDK handles this automatically in the background.
346+
182347
## Arkiv Topics/Features
183348

184349
### BTL
@@ -217,24 +382,6 @@ updated_entity = replace(
217382
client.arkiv.update_entity(updated_entity)
218383
```
219384

220-
### Query DSL
221-
222-
To make querying entities as simple and natural as possible, rely on a suitable and existing query DSL. Since Arkiv currently uses a SQL database backend and is likely to support SQL databases in the future, the Arkiv query DSL is defined as a **subset of the SQL standard**.
223-
224-
**Rationale:**
225-
- Leverages existing SQL knowledge - no new language to learn
226-
- Well-defined semantics and broad tooling support
227-
- Natural fit for relational data structures
228-
- Enables familiar filtering, joining, and aggregation patterns
229-
230-
**Example:**
231-
```python
232-
# Query entities using SQL-like syntax
233-
results = client.arkiv.query_entities(
234-
"SELECT key, payload WHERE attributes.type = 'user' AND attributes.age > 18 ORDER BY attributes.name"
235-
)
236-
```
237-
238385
### Sorting
239386

240387
Querying entities should support sorting results by one or more fields.

tests/test_query_iterator.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,90 @@ def test_iterate_entities_with_list(self, arkiv_client_http: Arkiv) -> None:
110110
# Verify all entity keys are present and unique
111111
result_keys = [entity.key for entity in entities]
112112
assert set(result_keys) == set(expected_keys)
113+
114+
def test_iterate_entities_empty(self, arkiv_client_http: Arkiv) -> None:
115+
"""Test basic iteration over multiple pages of entities."""
116+
# Create test entities
117+
num_entities = 10
118+
_, expected_keys = create_test_entities(arkiv_client_http, num_entities)
119+
120+
assert len(expected_keys) == num_entities
121+
122+
# Define query and options
123+
query = 'batch_id = "does not exist"'
124+
options = QueryOptions(fields=KEY | ATTRIBUTES, max_results_per_page=4)
125+
126+
# Classical for loop
127+
# Should get all 10 entities
128+
iterator = arkiv_client_http.arkiv.query_entities(query=query, options=options)
129+
entities = []
130+
for entity in iterator:
131+
entities.append(entity)
132+
133+
assert len(entities) == 0
134+
135+
def test_iterate_entities_less_than_page(self, arkiv_client_http: Arkiv) -> None:
136+
"""Test basic iteration over multiple pages of entities."""
137+
# Create test entities
138+
num_entities = 10
139+
batch_id, expected_keys = create_test_entities(arkiv_client_http, num_entities)
140+
141+
assert len(expected_keys) == num_entities
142+
143+
# Define query and options
144+
query = f'batch_id = "{batch_id}"'
145+
options = QueryOptions(
146+
fields=KEY | ATTRIBUTES, max_results_per_page=2 * num_entities
147+
)
148+
149+
# Classical for loop
150+
# Should get all 10 entities
151+
iterator = arkiv_client_http.arkiv.query_entities(query=query, options=options)
152+
entities = []
153+
for entity in iterator:
154+
entities.append(entity)
155+
156+
# Should get all 10 entities
157+
assert len(entities) == num_entities
158+
159+
# Verify all entities have the correct batch_id
160+
for entity in entities:
161+
assert entity.attributes is not None
162+
assert entity.attributes["batch_id"] == batch_id
163+
164+
# Verify all entity keys are present and unique
165+
result_keys = [entity.key for entity in entities]
166+
assert set(result_keys) == set(expected_keys)
167+
168+
def test_iterate_entities_exactly_page(self, arkiv_client_http: Arkiv) -> None:
169+
"""Test basic iteration over multiple pages of entities."""
170+
# Create test entities
171+
num_entities = 10
172+
batch_id, expected_keys = create_test_entities(arkiv_client_http, num_entities)
173+
174+
assert len(expected_keys) == num_entities
175+
176+
# Define query and options
177+
query = f'batch_id = "{batch_id}"'
178+
options = QueryOptions(
179+
fields=KEY | ATTRIBUTES, max_results_per_page=num_entities
180+
)
181+
182+
# Classical for loop
183+
# Should get all 10 entities
184+
iterator = arkiv_client_http.arkiv.query_entities(query=query, options=options)
185+
entities = []
186+
for entity in iterator:
187+
entities.append(entity)
188+
189+
# Should get all 10 entities
190+
assert len(entities) == num_entities
191+
192+
# Verify all entities have the correct batch_id
193+
for entity in entities:
194+
assert entity.attributes is not None
195+
assert entity.attributes["batch_id"] == batch_id
196+
197+
# Verify all entity keys are present and unique
198+
result_keys = [entity.key for entity in entities]
199+
assert set(result_keys) == set(expected_keys)

0 commit comments

Comments
 (0)