Skip to content

Commit da0ba06

Browse files
author
Matthias Zimmermann
committed
feat: add sorting functionality
1 parent c4c975e commit da0ba06

File tree

7 files changed

+298
-179
lines changed

7 files changed

+298
-179
lines changed

src/arkiv/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
from .node import ArkivNode
1010
from .query import QueryIterator
1111
from .types import (
12+
ASC,
13+
DESC,
14+
INT,
15+
STR,
1216
CreateEvent,
1317
DeleteEvent,
1418
ExtendEvent,
19+
OrderByAttribute,
1520
TransactionReceipt,
1621
UpdateEvent,
1722
)
@@ -23,6 +28,10 @@
2328
__version__ = "dev"
2429

2530
__all__ = [
31+
"ASC",
32+
"DESC",
33+
"INT",
34+
"STR",
2635
"Arkiv",
2736
"ArkivNode",
2837
"AsyncArkiv",
@@ -32,6 +41,7 @@
3241
"EventFilter",
3342
"ExtendEvent",
3443
"NamedAccount",
44+
"OrderByAttribute",
3545
"QueryIterator",
3646
"TransactionReceipt",
3747
"UpdateEvent",

src/arkiv/query.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ def __next__(self) -> Entity:
9191

9292
# Fetch next page if available
9393
if self._current_result.has_more() and not self._exhausted:
94-
options = QueryOptions(
95-
fields=self._options.fields,
94+
from dataclasses import replace
95+
96+
options = replace(
97+
self._options,
9698
at_block=self._current_result.block_number,
97-
max_results_per_page=self._options.max_results_per_page,
9899
cursor=self._current_result.cursor,
99100
)
100101
logger.info(
@@ -207,10 +208,11 @@ async def __anext__(self) -> Entity:
207208

208209
# Fetch next page if available
209210
if self._current_result.has_more() and not self._exhausted:
210-
options = QueryOptions(
211-
fields=self._options.fields,
211+
from dataclasses import replace
212+
213+
options = replace(
214+
self._options,
212215
at_block=self._current_result.block_number,
213-
max_results_per_page=self._options.max_results_per_page,
214216
cursor=self._current_result.cursor,
215217
)
216218
logger.info(

src/arkiv/types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,30 @@
4040
# Cursor type for entity set pagination for query results
4141
Cursor = NewType("Cursor", str)
4242

43+
# Order by type constants
44+
STR = "str"
45+
INT = "int"
46+
47+
# Order by direction constants
48+
ASC = "asc"
49+
DESC = "desc"
50+
51+
52+
@dataclass(frozen=True)
53+
class OrderByAttribute:
54+
"""Annotation for specifying order by fields in queries."""
55+
56+
attribute: str
57+
type: Literal["str", "int"]
58+
direction: Literal["asc", "desc"] = "asc"
59+
4360

4461
@dataclass(frozen=True)
4562
class QueryOptions:
4663
"""Options for querying entities."""
4764

4865
fields: int = ALL # Bitmask of fields to populate
66+
order_by: Sequence[OrderByAttribute] | None = None # Fields to order results by
4967
at_block: int | None = (
5068
None # Block number to pin query to specific block, or None to use latest block available
5169
)

src/arkiv/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
ATTRIBUTES,
3333
CONTENT_TYPE,
3434
CREATED_AT,
35+
DESC,
3536
EXPIRATION,
3637
KEY,
3738
LAST_MODIFIED_AT,
3839
MAX_RESULTS_PER_PAGE_DEFAULT,
3940
OP_INDEX_IN_TX,
4041
OWNER,
4142
PAYLOAD,
43+
STR,
4244
TX_INDEX_IN_BLOCK,
4345
Attributes,
4446
ChangeOwnerEvent,
@@ -298,6 +300,16 @@ def to_rpc_query_options(
298300
if options.cursor is not None:
299301
rpc_query_options["cursor"] = options.cursor
300302

303+
if options.order_by is not None:
304+
rpc_query_options["orderBy"] = [
305+
{
306+
"name": ob.attribute,
307+
"type": "string" if ob.type == STR else "numeric",
308+
"desc": ob.direction == DESC,
309+
}
310+
for ob in options.order_by
311+
]
312+
301313
return rpc_query_options
302314

303315

tests/test_query_language.py

Lines changed: 2 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,17 @@
11
"""Tests for Arkiv query language syntax and operators."""
22

33
import logging
4-
import uuid
54

65
from arkiv import Arkiv
7-
from arkiv.types import ATTRIBUTES, KEY, CreateOp, Operations, QueryOptions
6+
from arkiv.types import ATTRIBUTES, KEY, QueryOptions
87

9-
from .utils import to_create
8+
from .utils import create_test_entities
109

1110
OPTIONS = QueryOptions(fields=KEY | ATTRIBUTES, max_results_per_page=20)
1211

1312
logger = logging.getLogger(__name__)
1413

1514

16-
def add_to_c(c: list[CreateOp], a):
17-
c.append(to_create(attributes=a))
18-
19-
20-
def create_test_entities(client: Arkiv) -> tuple[str, list[str]]:
21-
"""
22-
Create a batch of test entities with sequential numeric attributes.
23-
"""
24-
batch = str(uuid.uuid4())
25-
26-
# Build list of CreateOp operations
27-
c: list[CreateOp] = []
28-
add_to_c(
29-
c,
30-
{
31-
"batch": batch,
32-
"id": 1,
33-
"type": "A",
34-
"size": "xs",
35-
"idx": 1,
36-
"email": "[email protected]",
37-
},
38-
)
39-
add_to_c(
40-
c,
41-
{
42-
"batch": batch,
43-
"id": 2,
44-
"type": "A",
45-
"size": "s",
46-
"idx": 2,
47-
"email": "[email protected]",
48-
},
49-
)
50-
add_to_c(
51-
c,
52-
{
53-
"batch": batch,
54-
"id": 3,
55-
"type": "A",
56-
"size": "m",
57-
"idx": 3,
58-
"email": "[email protected]",
59-
},
60-
)
61-
add_to_c(
62-
c,
63-
{
64-
"batch": batch,
65-
"id": 4,
66-
"type": "A",
67-
"size": "l",
68-
"idx": 4,
69-
"email": "[email protected]",
70-
},
71-
)
72-
73-
add_to_c(
74-
c,
75-
{
76-
"batch": batch,
77-
"id": 5,
78-
"type": "B",
79-
"size": "xs",
80-
"idx": 1,
81-
"email": "[email protected]",
82-
},
83-
)
84-
add_to_c(
85-
c,
86-
{
87-
"batch": batch,
88-
"id": 6,
89-
"type": "B",
90-
"size": "s",
91-
"idx": 2,
92-
"email": "[email protected]",
93-
},
94-
)
95-
add_to_c(
96-
c,
97-
{
98-
"batch": batch,
99-
"id": 7,
100-
"type": "B",
101-
"size": "m",
102-
"idx": 3,
103-
"email": "[email protected]",
104-
},
105-
)
106-
add_to_c(
107-
c,
108-
{
109-
"batch": batch,
110-
"id": 8,
111-
"type": "B",
112-
"size": "l",
113-
"idx": 4,
114-
"email": "[email protected]",
115-
},
116-
)
117-
118-
add_to_c(
119-
c,
120-
{
121-
"batch": batch,
122-
"id": 9,
123-
"type": "C",
124-
"size": "xs",
125-
"idx": 1,
126-
"email": "[email protected]",
127-
},
128-
)
129-
add_to_c(
130-
c,
131-
{
132-
"batch": batch,
133-
"id": 10,
134-
"type": "C",
135-
"size": "s",
136-
"idx": 2,
137-
"email": "[email protected]",
138-
},
139-
)
140-
add_to_c(
141-
c,
142-
{
143-
"batch": batch,
144-
"id": 11,
145-
"type": "C",
146-
"size": "m",
147-
"idx": 3,
148-
"email": "[email protected]",
149-
},
150-
)
151-
add_to_c(
152-
c,
153-
{
154-
"batch": batch,
155-
"id": 12,
156-
"type": "C",
157-
"size": "l",
158-
"idx": 4,
159-
"email": "[email protected]",
160-
},
161-
)
162-
163-
# Execute all creates in a single transaction
164-
operations = Operations(creates=c)
165-
receipt = client.arkiv.execute(operations)
166-
167-
# Extract entity keys from receipt
168-
entity_keys = [create.key for create in receipt.creates]
169-
assert len(entity_keys) == 12
170-
171-
return batch, entity_keys
172-
173-
17415
def execute_query_test(
17516
client: Arkiv, label: str, query: str, expected_ids: list[int]
17617
) -> None:

0 commit comments

Comments
 (0)