Skip to content

Commit ecb718b

Browse files
connect() timeout validation + copy cursor.description on read
ISSUE (cycle 6) — dqlitedbapi.connect() accepted any timeout value (negative, NaN, infinite) and quietly passed it down. Now rejects non-positive / non-finite values up front with ValueError, matching DqliteConnection's validation. ISSUE (cycle 6) — cursor.description was returning the internal list by reference. A caller mutating it (cursor.description.clear(), cursor.description[0] = ...) corrupted the cursor's state. Now returns a fresh shallow copy per access in both sync and async cursors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7815cbd commit ecb718b

File tree

3 files changed

+23
-4
lines changed

3 files changed

+23
-4
lines changed

src/dqlitedbapi/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,16 @@ def connect(
9393
Args:
9494
address: Node address in "host:port" format
9595
database: Database name to open
96-
timeout: Connection timeout in seconds
96+
timeout: Connection timeout in seconds — must be a positive
97+
finite number. ``0``, negatives, and non-finite values are
98+
rejected here rather than silently passed through to the
99+
underlying connection.
97100
98101
Returns:
99102
A Connection object
100103
"""
104+
import math
105+
106+
if not math.isfinite(timeout) or timeout <= 0:
107+
raise ValueError(f"timeout must be a positive finite number, got {timeout}")
101108
return Connection(address, database=database, timeout=timeout)

src/dqlitedbapi/aio/cursor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ def __init__(self, connection: "AsyncConnection") -> None:
3535
def description(
3636
self,
3737
) -> list[tuple[str, int | None, None, None, None, None, None]] | None:
38-
"""Column descriptions for the last query."""
39-
return self._description
38+
"""Column descriptions for the last query.
39+
40+
Returns a fresh shallow copy so a caller can't corrupt internal
41+
cursor state by mutating the returned list.
42+
"""
43+
if self._description is None:
44+
return None
45+
return list(self._description)
4046

4147
@property
4248
def rowcount(self) -> int:

src/dqlitedbapi/cursor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,14 @@ def description(
146146
``type_code`` is the wire-level ``ValueType`` integer from the first
147147
result frame (e.g. 10 for ISO8601, 9 for UNIXTIME). The other fields
148148
are None — dqlite doesn't expose them.
149+
150+
Returns a fresh shallow copy each call so that a caller
151+
mutating the list (e.g. ``cursor.description.clear()``) can't
152+
corrupt the cursor's internal state.
149153
"""
150-
return self._description
154+
if self._description is None:
155+
return None
156+
return list(self._description)
151157

152158
@property
153159
def rowcount(self) -> int:

0 commit comments

Comments
 (0)