"""Keyset pagination tests: cursor values are restored to Python types. Cursors are JSON, so a cursor carrying a datetime reaches the next request as a string. Postgres/asyncpg rejects strings for timestamp parameters (sqlite silently coerces them), so ``paginate`` must run the decoded values through their model fields before building the WHERE clause. """ from __future__ import annotations import unittest import uuid from datetime import datetime, timezone from typing import Any, List from tavolo.platform.models import Match from tavolo.platform.pagination import Cursor, decode_cursor, encode_cursor, paginate from helpers import async_test SORT = [("finished_at", "DESC"), ("id", "ASC")] class _CapturingQuerySet: """Minimal stand-in for a Tortoise QuerySet, recording its filter.""" model = Match def __init__(self) -> None: self.queryset_filter: Any = None def order_by(self, *orderings: str) -> "_CapturingQuerySet": return self def filter(self, q: Any) -> "_CapturingQuerySet": self.queryset_filter = q return self async def limit(self, count: int) -> List[Any]: return [] def _flatten_q(q: Any) -> List[Any]: nodes = [q] for child in q.children: nodes.extend(_flatten_q(child)) return nodes def _filter_values(q: Any, key: str) -> List[Any]: return [node.filters[key] for node in _flatten_q(q) if key in node.filters] class PaginateCursorTypesTest(unittest.TestCase): @async_test async def test_cursor_values_are_restored_before_filtering(self) -> None: match_id = uuid.uuid4() finished_at = datetime(2026, 9, 19, 5, 2, 50, 547904, tzinfo=timezone.utc) # Round-trip through the wire format, exactly as a request does. after = decode_cursor( encode_cursor({"finished_at": finished_at, "id": match_id}) ) assert after is not None qs = _CapturingQuerySet() await paginate(qs, SORT, Cursor(limit=1, after=after)) assert qs.queryset_filter is not None for key in ("finished_at", "finished_at__lt"): values = _filter_values(qs.queryset_filter, key) self.assertEqual(1, len(values)) self.assertIsInstance(values[0], datetime) self.assertEqual(finished_at, values[0]) id_values = _filter_values(qs.queryset_filter, "id__gt") self.assertEqual(1, len(id_values)) self.assertIsInstance(id_values[0], uuid.UUID) self.assertEqual(match_id, id_values[0]) if __name__ == "__main__": unittest.main()