Skip to content

Commit cb18033

Browse files
Drop false stdlib-parity claim from fetchmany(0) docstring
Cursor.fetchmany / AsyncCursor.fetchmany advertised "stdlib sqlite3 parity" for the size=0 corner case. That claim is not reliable: stdlib's fetchmany(0) behaviour varies across Python / sqlite releases (some drain the result set, others return []), so dqlite returning [] is parity with some versions and divergence from others. The parity claim leaks a guarantee dqlite cannot keep. Replace the parity wording with an explicit cross-driver matrix that names what dqlite does (returns [] deterministically), how stdlib's behaviour is version-dependent, and how psycopg3 treats 0 as a "use arraysize" sentinel. Cross-driver porters now have the matrix instead of a false parity claim. Documentation-only; no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9193431 commit cb18033

3 files changed

Lines changed: 92 additions & 21 deletions

File tree

src/dqlitedbapi/aio/cursor.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -708,14 +708,19 @@ async def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
708708
709709
Returns an empty list when no more rows are available OR when
710710
no result set is active (DML-only / never-executed). Stdlib
711-
parity with ``sqlite3.Cursor.fetchmany`` matching the
712-
``fetchone`` parity already in place.
713-
714-
**Divergence from psycopg3**: an explicit ``size=0`` returns
715-
``[]`` here (stdlib ``sqlite3`` parity). psycopg3 treats
716-
``size=0`` as "use ``self.arraysize``". Pass ``None`` or
717-
omit ``size`` to default to ``self.arraysize``. See sync
718-
sibling for the rationale.
711+
parity with ``sqlite3.Cursor.fetchmany`` for the "no result
712+
set" case matching the ``fetchone`` parity already in place.
713+
714+
**``size=0`` divergence (cross-driver matrix)**: dqlite
715+
returns ``[]`` deterministically. This differs from stdlib
716+
``sqlite3`` (whose behaviour for ``fetchmany(0)`` is
717+
version-dependent — some Python/sqlite releases drain the
718+
result set, others return ``[]``) and from psycopg3 (which
719+
treats ``0`` as the sentinel "use ``self.arraysize``").
720+
Cross-driver code should pass an explicit positive size,
721+
use ``None`` / omit ``size`` to default to ``self.arraysize``,
722+
or rely on ``fetchall()`` to drain. See sync sibling for the
723+
full rationale.
719724
"""
720725
del self.messages[:]
721726
self._check_closed()

src/dqlitedbapi/cursor.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,20 +1778,26 @@ def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
17781778
Returns an empty list when no more rows are available. ``size``
17791779
defaults to ``self.arraysize``.
17801780
1781-
Stdlib parity: ``sqlite3.Cursor.fetchmany()`` after a DML
1782-
(no result set active) returns ``[]`` rather than raising,
1783-
matching the ``fetchone`` parity already in place. Cross-
1784-
driver code that polls ``cur.fetchmany(N) or default`` after
1785-
a connect-and-cursor sequence works on stdlib and dqlite.
1786-
1787-
**Divergence from psycopg3**: an explicit ``size=0`` returns
1788-
``[]`` here (stdlib ``sqlite3`` parity). psycopg3 treats
1789-
``size=0`` as the sentinel meaning "use ``self.arraysize``"
1790-
(its default IS ``size: int = 0``, not ``None``). Cross-
1791-
driver code ported from psycopg that calls
1781+
Stdlib parity for the "no result set" case:
1782+
``sqlite3.Cursor.fetchmany()`` after a DML (no result set
1783+
active) returns ``[]`` rather than raising, matching the
1784+
``fetchone`` parity already in place. Cross-driver code that
1785+
polls ``cur.fetchmany(N) or default`` after a connect-and-
1786+
cursor sequence works on stdlib and dqlite.
1787+
1788+
**``size=0`` divergence (cross-driver matrix)**: dqlite
1789+
returns ``[]`` deterministically. This differs from stdlib
1790+
``sqlite3`` (which has historically drained the result set on
1791+
some Python/sqlite versions and returned ``[]`` on others —
1792+
the behaviour is version-dependent and was never a reliable
1793+
parity guarantee) and from psycopg3 (which treats ``0`` as
1794+
the sentinel "use ``self.arraysize``", since its default IS
1795+
``size: int = 0``, not ``None``). Cross-driver code should
1796+
pass an explicit positive size, use ``None`` / omit the
1797+
argument to default to ``self.arraysize``, or rely on
1798+
``fetchall()`` to drain. Code ported from psycopg that calls
17921799
``cur.fetchmany(0)`` thinking it requests "default batch"
1793-
gets an empty list under dqlite. Pass ``None`` or omit
1794-
``size`` to default to ``self.arraysize``.
1800+
gets an empty list under dqlite.
17951801
"""
17961802
del self.messages[:]
17971803
# See ``execute``'s prelude comment for the ordering rationale.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Doc-pin: ``Cursor.fetchmany`` / ``AsyncCursor.fetchmany`` docstrings
2+
do NOT claim "stdlib sqlite3 parity" for the ``size=0`` corner case.
3+
4+
The prior docstring asserted "stdlib sqlite3 parity" for the
5+
``size=0`` corner; that claim has shifted across CPython releases
6+
(older Python releases drained the result set on ``fetchmany(0)``,
7+
recent releases return ``[]``) and was never a guarantee dqlite
8+
could meaningfully make. The replacement wording drops the
9+
parity claim and explicitly documents the three-driver matrix —
10+
stdlib behaviour varies / psycopg3 treats ``0`` as "use arraysize"
11+
/ dqlite returns ``[]`` deterministically.
12+
13+
A doc-pin is sufficient because there is no behaviour change — this
14+
issue is documentation-only. The behaviour pin lives at
15+
``test_fetchmany_edges.py``; here we only assert that the docstring
16+
does not perpetuate the false-parity claim and explicitly flags the
17+
divergence.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import dqlitedbapi
23+
import dqlitedbapi.aio
24+
25+
26+
def test_sync_fetchmany_docstring_drops_stdlib_parity_claim() -> None:
27+
doc = (dqlitedbapi.Cursor.fetchmany.__doc__ or "").lower()
28+
assert "size=0" in doc or "size == 0" in doc, (
29+
"fetchmany docstring should document the size=0 corner case "
30+
"explicitly so the divergence callout has a referent."
31+
)
32+
# The old wording said "stdlib sqlite3 parity" for size=0 which is
33+
# factually false / version-dependent. The new wording must
34+
# explicitly flag the divergence.
35+
assert "differs from stdlib" in doc or "diverges from stdlib" in doc, (
36+
"fetchmany docstring must explicitly flag the stdlib divergence "
37+
"for size=0 — stdlib's behaviour is version-dependent and was "
38+
"never a reliable parity claim."
39+
)
40+
# Defensive: the literal "stdlib sqlite3 parity" substring (with
41+
# the conjoined wording) should no longer appear.
42+
assert "stdlib sqlite3 parity" not in doc, (
43+
"fetchmany docstring still contains the 'stdlib sqlite3 parity' "
44+
"phrase that this fix was supposed to remove."
45+
)
46+
47+
48+
def test_async_fetchmany_docstring_drops_stdlib_parity_claim() -> None:
49+
doc = (dqlitedbapi.aio.AsyncCursor.fetchmany.__doc__ or "").lower()
50+
assert "size=0" in doc or "size == 0" in doc, (
51+
"AsyncCursor.fetchmany docstring should document the size=0 corner case."
52+
)
53+
assert "differs from stdlib" in doc or "diverges from stdlib" in doc, (
54+
"AsyncCursor.fetchmany docstring must explicitly flag the stdlib "
55+
"divergence for size=0 (mirrors the sync sibling)."
56+
)
57+
assert "stdlib sqlite3 parity" not in doc, (
58+
"AsyncCursor.fetchmany docstring still contains the 'stdlib sqlite3 "
59+
"parity' phrase that this fix was supposed to remove."
60+
)

0 commit comments

Comments
 (0)