Skip to content

Commit 8f141c9

Browse files
Add optional PEP 249 methods to AsyncAdaptedCursor
The greenlet adapter implemented the hot-path cursor surface but left four PEP 249 optional extensions unhandled: callproc, nextset, scroll, and the `connection` property. Missing them means a consumer that expects `cursor.nextset()` to raise NotSupportedError (as both the sync Cursor and async AsyncCursor do) gets AttributeError instead. Add the three NotSupportedError-raising methods and the connection property so the adapter presents a consistent optional-extension surface. Skip `rownumber`: the adapter buffers rows in a deque that is popped left as the user consumes them, so a truthful counter would need parallel state increments on every fetch path. Consumers who need rownumber should use AsyncCursor directly — documented inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 94f1295 commit 8f141c9

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

src/sqlalchemydqlite/aio.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sqlalchemy.util import await_only
1212

1313
from dqliteclient.exceptions import DqliteConnectionError
14-
from dqlitedbapi.exceptions import InterfaceError, OperationalError
14+
from dqlitedbapi.exceptions import InterfaceError, NotSupportedError, OperationalError
1515
from sqlalchemydqlite.base import DqliteDialect
1616

1717
__all__ = ["DqliteDialect_aio"]
@@ -105,6 +105,36 @@ def setinputsizes(self, *inputsizes: Any) -> None:
105105
def setoutputsize(self, size: int, column: int | None = None) -> None:
106106
pass
107107

108+
@property
109+
def connection(self) -> "AsyncAdaptedConnection":
110+
"""The AsyncAdaptedConnection this cursor was created from.
111+
112+
PEP 249 optional extension mirroring Cursor.connection /
113+
AsyncCursor.connection. Read-only.
114+
"""
115+
return self._adapt_connection
116+
117+
# PEP 249 optional extensions. The non-adapter cursors raise
118+
# NotSupportedError for these same calls; do the same here so a
119+
# consumer catching NotSupportedError behaves consistently whether it
120+
# is handed an AsyncCursor or a greenlet-wrapped AsyncAdaptedCursor.
121+
#
122+
# `rownumber` is deliberately NOT implemented: the adapter buffers
123+
# rows into a deque that is popped left on consumption, so a truthful
124+
# counter would need parallel state increments in fetchone /
125+
# fetchmany / fetchall / __next__. Consumers who need rownumber
126+
# should use AsyncCursor directly.
127+
def callproc(
128+
self, procname: str, parameters: Sequence[Any] | None = None
129+
) -> Sequence[Any] | None:
130+
raise NotSupportedError("dqlite does not support stored procedures")
131+
132+
def nextset(self) -> bool | None:
133+
raise NotSupportedError("dqlite does not support multiple result sets")
134+
135+
def scroll(self, value: int, mode: str = "relative") -> None:
136+
raise NotSupportedError("dqlite cursors are not scrollable")
137+
108138
def __iter__(self) -> Any:
109139
while self._rows:
110140
yield self._rows.popleft()

tests/test_async_adapter.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,39 @@ def _has_finally_with_close(func: object) -> bool:
8989
return False
9090

9191

92+
class TestAsyncAdaptedCursorOptionalMethods:
93+
def test_connection_property_returns_adapter(self) -> None:
94+
cursor = _make_cursor()
95+
assert cursor.connection is cursor._adapt_connection
96+
97+
def test_callproc_raises_not_supported(self) -> None:
98+
import pytest
99+
100+
from dqlitedbapi.exceptions import NotSupportedError
101+
102+
cursor = _make_cursor()
103+
with pytest.raises(NotSupportedError):
104+
cursor.callproc("sp_foo")
105+
106+
def test_nextset_raises_not_supported(self) -> None:
107+
import pytest
108+
109+
from dqlitedbapi.exceptions import NotSupportedError
110+
111+
cursor = _make_cursor()
112+
with pytest.raises(NotSupportedError):
113+
cursor.nextset()
114+
115+
def test_scroll_raises_not_supported(self) -> None:
116+
import pytest
117+
118+
from dqlitedbapi.exceptions import NotSupportedError
119+
120+
cursor = _make_cursor()
121+
with pytest.raises(NotSupportedError):
122+
cursor.scroll(5)
123+
124+
92125
class TestAsyncAdaptedCursorCleanup:
93126
def test_cursor_closed_on_execute_error(self) -> None:
94127
"""Underlying cursor must be closed even if execute() raises."""

0 commit comments

Comments
 (0)