Skip to content

Commit a14feb1

Browse files
committed
Completing cursor tests
1 parent 72b0a2b commit a14feb1

File tree

3 files changed

+219
-6
lines changed

3 files changed

+219
-6
lines changed

arangoasync/aql.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def execute(
5353
memory_limit: Optional[int] = None,
5454
ttl: Optional[int] = None,
5555
allow_dirty_read: Optional[bool] = None,
56-
options: Optional[QueryProperties] = None,
56+
options: Optional[QueryProperties | Json] = None,
5757
) -> Result[Cursor]:
5858
"""Execute the query and return the result cursor.
5959
@@ -73,7 +73,7 @@ async def execute(
7373
will be removed on the server automatically after the specified amount
7474
of time.
7575
allow_dirty_read (bool | None): Allow reads from followers in a cluster.
76-
options (QueryProperties | None): Extra options for the query.
76+
options (QueryProperties | dict | None): Extra options for the query.
7777
7878
References:
7979
- `create-a-cursor <https://docs.arangodb.com/stable/develop/http-api/queries/aql-queries/#create-a-cursor>`__
@@ -92,7 +92,9 @@ async def execute(
9292
if ttl is not None:
9393
data["ttl"] = ttl
9494
if options is not None:
95-
data["options"] = options.to_dict()
95+
if isinstance(options, QueryProperties):
96+
options = options.to_dict()
97+
data["options"] = options
9698

9799
headers = dict()
98100
if allow_dirty_read is not None:

arangoasync/cursor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __init__(self, executor: ApiExecutor, data: Json) -> None:
5050
self._batch: Deque[Any] = deque()
5151
self._update(data)
5252

53-
async def __aiter__(self) -> "Cursor":
53+
def __aiter__(self) -> "Cursor":
5454
return self
5555

5656
async def __anext__(self) -> Any:

tests/test_cursor.py

+213-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
import pytest
44

55
from arangoasync.aql import AQL
6-
from arangoasync.errno import CURSOR_NOT_FOUND
7-
from arangoasync.exceptions import CursorCloseError
6+
from arangoasync.errno import CURSOR_NOT_FOUND, HTTP_BAD_PARAMETER
7+
from arangoasync.exceptions import (
8+
CursorCloseError,
9+
CursorCountError,
10+
CursorEmptyError,
11+
CursorNextError,
12+
CursorStateError,
13+
)
814
from arangoasync.typings import QueryExecutionStats, QueryProperties
915

1016

@@ -180,3 +186,208 @@ async def test_cursor_write_query(db, doc_col, docs):
180186
await cursor.close(ignore_missing=False)
181187
assert err.value.error_code == CURSOR_NOT_FOUND
182188
assert await cursor.close(ignore_missing=True) is False
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_cursor_invalid_id(db, doc_col, docs):
193+
# Insert documents
194+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
195+
196+
aql: AQL = db.aql
197+
cursor = await aql.execute(
198+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
199+
count=True,
200+
batch_size=2,
201+
ttl=1000,
202+
options={"optimizer": {"rules": ["+all"]}, "profile": 1},
203+
)
204+
205+
# Set the cursor ID to "invalid" and assert errors
206+
setattr(cursor, "_id", "invalid")
207+
208+
# Cursor should not be found
209+
with pytest.raises(CursorNextError) as err:
210+
async for _ in cursor:
211+
pass
212+
assert err.value.error_code == CURSOR_NOT_FOUND
213+
with pytest.raises(CursorCloseError) as err:
214+
await cursor.close(ignore_missing=False)
215+
assert err.value.error_code == CURSOR_NOT_FOUND
216+
assert await cursor.close(ignore_missing=True) is False
217+
218+
# Set the cursor ID to None and assert errors
219+
setattr(cursor, "_id", None)
220+
with pytest.raises(CursorStateError):
221+
print(await cursor.next())
222+
with pytest.raises(CursorStateError):
223+
await cursor.fetch()
224+
assert await cursor.close() is False
225+
226+
227+
@pytest.mark.asyncio
228+
async def test_cursor_premature_close(db, doc_col, docs):
229+
# Insert documents
230+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
231+
232+
aql: AQL = db.aql
233+
cursor = await aql.execute(
234+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
235+
count=True,
236+
batch_size=2,
237+
ttl=1000,
238+
)
239+
assert len(cursor.batch) == 2
240+
assert await cursor.close() is True
241+
242+
# Cursor should be already closed
243+
with pytest.raises(CursorCloseError) as err:
244+
await cursor.close(ignore_missing=False)
245+
assert err.value.error_code == CURSOR_NOT_FOUND
246+
assert await cursor.close(ignore_missing=True) is False
247+
248+
249+
@pytest.mark.asyncio
250+
async def test_cursor_context_manager(db, doc_col, docs):
251+
# Insert documents
252+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
253+
254+
aql: AQL = db.aql
255+
cursor = await aql.execute(
256+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
257+
count=True,
258+
batch_size=2,
259+
ttl=1000,
260+
)
261+
async with cursor as ctx:
262+
assert (await ctx.next())["val"] == docs[0]["val"]
263+
264+
# Cursor should be already closed
265+
with pytest.raises(CursorCloseError) as err:
266+
await cursor.close(ignore_missing=False)
267+
assert err.value.error_code == CURSOR_NOT_FOUND
268+
assert await cursor.close(ignore_missing=True) is False
269+
270+
271+
@pytest.mark.asyncio
272+
async def test_cursor_manual_fetch_and_pop(db, doc_col, docs):
273+
# Insert documents
274+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
275+
276+
aql: AQL = db.aql
277+
cursor = await aql.execute(
278+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
279+
count=True,
280+
batch_size=1,
281+
ttl=1000,
282+
options={"allowRetry": True},
283+
)
284+
285+
# Fetch documents manually
286+
for idx in range(2, len(docs)):
287+
result = await cursor.fetch()
288+
assert len(result) == 1
289+
assert cursor.count == len(docs)
290+
assert cursor.has_more
291+
assert len(cursor.batch) == idx
292+
assert result[0]["val"] == docs[idx - 1]["val"]
293+
result = await cursor.fetch()
294+
assert result[0]["val"] == docs[len(docs) - 1]["val"]
295+
assert len(cursor.batch) == len(docs)
296+
assert not cursor.has_more
297+
298+
# Pop documents manually
299+
idx = 0
300+
while not cursor.empty():
301+
doc = cursor.pop()
302+
assert doc["val"] == docs[idx]["val"]
303+
idx += 1
304+
assert len(cursor.batch) == 0
305+
306+
# Cursor should be empty
307+
with pytest.raises(CursorEmptyError):
308+
await cursor.pop()
309+
310+
311+
@pytest.mark.asyncio
312+
async def test_cursor_retry(db, doc_col, docs):
313+
# Insert documents
314+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
315+
316+
# Do not allow retries
317+
aql: AQL = db.aql
318+
cursor = await aql.execute(
319+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
320+
count=True,
321+
batch_size=1,
322+
ttl=1000,
323+
options={"allowRetry": False},
324+
)
325+
326+
# Increase the batch id by doing a fetch
327+
await cursor.fetch()
328+
while not cursor.empty():
329+
cursor.pop()
330+
next_batch_id = cursor.next_batch_id
331+
332+
# Fetch the next batch
333+
await cursor.fetch()
334+
# Retry is not allowed
335+
with pytest.raises(CursorNextError) as err:
336+
await cursor.fetch(batch_id=next_batch_id)
337+
assert err.value.error_code == HTTP_BAD_PARAMETER
338+
339+
await cursor.close()
340+
341+
# Now let's allow retries
342+
cursor = await aql.execute(
343+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
344+
count=True,
345+
batch_size=1,
346+
ttl=1000,
347+
options={"allowRetry": True},
348+
)
349+
350+
# Increase the batch id by doing a fetch
351+
await cursor.fetch()
352+
while not cursor.empty():
353+
cursor.pop()
354+
next_batch_id = cursor.next_batch_id
355+
356+
# Fetch the next batch
357+
prev_batch = await cursor.fetch()
358+
next_next_batch_id = cursor.next_batch_id
359+
# Should fetch the same batch again
360+
next_batch = await cursor.fetch(batch_id=next_batch_id)
361+
assert next_batch == prev_batch
362+
# Next batch id should be the same
363+
assert cursor.next_batch_id == next_next_batch_id
364+
365+
# Fetch the next batch
366+
next_next_batch = await cursor.fetch()
367+
assert next_next_batch != next_batch
368+
369+
assert await cursor.close()
370+
371+
372+
@pytest.mark.asyncio
373+
async def test_cursor_no_count(db, doc_col, docs):
374+
# Insert documents
375+
await asyncio.gather(*[doc_col.insert(doc) for doc in docs])
376+
377+
aql: AQL = db.aql
378+
cursor = await aql.execute(
379+
f"FOR d IN {doc_col.name} SORT d._key RETURN d",
380+
count=False,
381+
batch_size=2,
382+
ttl=1000,
383+
)
384+
385+
# Cursor count is not enabled
386+
with pytest.raises(CursorCountError):
387+
_ = len(cursor)
388+
with pytest.raises(CursorCountError):
389+
_ = bool(cursor)
390+
391+
while cursor.has_more:
392+
assert cursor.count is None
393+
assert await cursor.fetch()

0 commit comments

Comments
 (0)