Skip to content

Commit fadaebe

Browse files
authored
0.4.4 - Bug fix and add retry (#21)
* Fix bugs * Add retry mechanism --------- Co-authored-by: Peng Ren
1 parent 23b5d02 commit fadaebe

13 files changed

Lines changed: 475 additions & 77 deletions

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ PyMongoSQL implements the DB API 2.0 interfaces to provide SQL-like access to Mo
4949
- **JMESPath** (JSON/Dict Path Query)
5050
- jmespath >= 1.0.0
5151

52+
- **Tenacity** (Transient Failure Retry)
53+
- tenacity >= 9.0.0
54+
5255
### Optional Dependencies
5356

5457
- **SQLAlchemy** (for ORM/Core support)
@@ -206,6 +209,22 @@ cursor.execute(
206209

207210
Parameters are substituted into the MongoDB filter during execution, providing protection against injection attacks.
208211

212+
### Retry on Transient System Errors
213+
214+
PyMongoSQL supports retrying transient, system-level MongoDB failures (for example connection timeout and reconnect errors) using Tenacity.
215+
216+
```python
217+
connection = connect(
218+
host="mongodb://localhost:27017/database",
219+
retry_enabled=True, # default: True
220+
retry_attempts=3, # default: 3
221+
retry_wait_min=0.1, # default: 0.1 seconds
222+
retry_wait_max=1.0, # default: 1.0 seconds
223+
)
224+
```
225+
226+
These options apply to connection ping checks, query/DML command execution, and paginated `getMore` fetches.
227+
209228
## Supported SQL Features
210229

211230
### SELECT Statements

pymongosql/__init__.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if TYPE_CHECKING:
77
from .connection import Connection
88

9-
__version__: str = "0.4.3"
9+
__version__: str = "0.4.4"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"
@@ -36,6 +36,115 @@ def __hash__(self):
3636
return frozenset.__hash__(self)
3737

3838

39+
# DB API 2.0 Type Objects for MongoDB Data Types
40+
# https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
41+
# Mapping of MongoDB BSON types to DB API 2.0 type objects
42+
43+
# Null/None type
44+
NULL = DBAPITypeObject(("null", "Null", "NULL"))
45+
46+
# String types
47+
STRING = DBAPITypeObject(("string", "str", "String", "VARCHAR", "CHAR", "TEXT"))
48+
49+
# Numeric types - Integer
50+
BINARY = DBAPITypeObject(("binary", "Binary", "BINARY", "VARBINARY", "BLOB", "ObjectId"))
51+
52+
# Numeric types - Integer
53+
NUMBER = DBAPITypeObject(("int", "integer", "long", "int32", "int64", "Integer", "BIGINT", "INT"))
54+
55+
# Numeric types - Decimal/Float
56+
FLOAT = DBAPITypeObject(("double", "decimal", "float", "Double", "DECIMAL", "FLOAT", "NUMERIC"))
57+
58+
# Boolean type
59+
BOOLEAN = DBAPITypeObject(("bool", "boolean", "Bool", "BOOLEAN"))
60+
61+
# Date/Time types
62+
DATE = DBAPITypeObject(("date", "Date", "DATE"))
63+
TIME = DBAPITypeObject(("time", "Time", "TIME"))
64+
DATETIME = DBAPITypeObject(("datetime", "timestamp", "Timestamp", "DATETIME", "TIMESTAMP"))
65+
66+
# Aggregate types
67+
ARRAY = DBAPITypeObject(("array", "Array", "ARRAY", "list"))
68+
OBJECT = DBAPITypeObject(("object", "Object", "OBJECT", "struct", "dict", "document"))
69+
70+
# Special MongoDB types
71+
OBJECTID = DBAPITypeObject(("objectid", "ObjectId", "OBJECTID", "oid"))
72+
REGEX = DBAPITypeObject(("regex", "Regex", "REGEX", "regexp"))
73+
74+
# Map MongoDB BSON type codes to DB API type objects
75+
# This mapping helps cursor.description identify the correct type for each column
76+
_MONGODB_TYPE_MAP = {
77+
"null": NULL,
78+
"string": STRING,
79+
"int": NUMBER,
80+
"integer": NUMBER,
81+
"long": NUMBER,
82+
"int32": NUMBER,
83+
"int64": NUMBER,
84+
"double": FLOAT,
85+
"decimal": FLOAT,
86+
"float": FLOAT,
87+
"bool": BOOLEAN,
88+
"boolean": BOOLEAN,
89+
"date": DATE,
90+
"datetime": DATETIME,
91+
"timestamp": DATETIME,
92+
"array": ARRAY,
93+
"object": OBJECT,
94+
"document": OBJECT,
95+
"bson.objectid": OBJECTID,
96+
"objectid": OBJECTID,
97+
"regex": REGEX,
98+
"binary": BINARY,
99+
}
100+
101+
102+
def get_type_code(value: object) -> str:
103+
"""Get the type code for a MongoDB value.
104+
105+
Maps a MongoDB/Python value to its corresponding DB API type code string.
106+
107+
Args:
108+
value: The value to determine the type for
109+
110+
Returns:
111+
A string representing the DB API type code
112+
"""
113+
if value is None:
114+
return "null"
115+
elif isinstance(value, bool):
116+
return "bool"
117+
elif isinstance(value, int):
118+
return "int"
119+
elif isinstance(value, float):
120+
return "double"
121+
elif isinstance(value, str):
122+
return "string"
123+
elif isinstance(value, bytes):
124+
return "binary"
125+
elif isinstance(value, dict):
126+
return "object"
127+
elif isinstance(value, list):
128+
return "array"
129+
elif hasattr(value, "__class__") and value.__class__.__name__ == "ObjectId":
130+
return "objectid"
131+
else:
132+
return "object"
133+
134+
135+
def get_type_object(value: object) -> DBAPITypeObject:
136+
"""Get the DB API type object for a MongoDB value.
137+
138+
Args:
139+
value: The value to get type information for
140+
141+
Returns:
142+
A DBAPITypeObject representing the value's type
143+
"""
144+
type_code = get_type_code(value)
145+
return _MONGODB_TYPE_MAP.get(type_code, OBJECT)
146+
147+
39148
def connect(*args, **kwargs) -> "Connection":
40149
from .connection import Connection
41150

pymongosql/connection.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .cursor import Cursor
1414
from .error import DatabaseError, OperationalError
1515
from .helper import ConnectionHelper
16+
from .retry import RetryConfig, execute_with_retry
1617

1718
_logger = logging.getLogger(__name__)
1819

@@ -55,6 +56,10 @@ def __init__(
5556
if not self._mode and mode:
5657
self._mode = mode
5758

59+
# Retry behavior for transient system-level PyMongo failures.
60+
# These kwargs are consumed by PyMongoSQL and are not passed to MongoClient.
61+
self._retry_config = RetryConfig.from_kwargs(kwargs)
62+
5863
# Extract commonly used parameters for backward compatibility
5964
self._host = host or "localhost"
6065
self._port = port or 27017
@@ -109,7 +114,11 @@ def _connect(self) -> None:
109114
self._client = MongoClient(**self._pymongo_params)
110115

111116
# Test connection
112-
self._client.admin.command("ping")
117+
execute_with_retry(
118+
lambda: self._client.admin.command("ping"),
119+
self._retry_config,
120+
"initial MongoDB ping",
121+
)
113122

114123
# Initialize the database according to explicit parameter or client's default
115124
# This may raise OperationalError if no database could be determined; allow it to bubble up
@@ -179,6 +188,11 @@ def mode(self) -> str:
179188
"""Get the specified mode"""
180189
return self._mode
181190

191+
@property
192+
def retry_config(self) -> RetryConfig:
193+
"""Get retry configuration used for transient system-level errors."""
194+
return self._retry_config
195+
182196
def use_database(self, database_name: str) -> None:
183197
"""Switch to a different database"""
184198
if self._client is None:
@@ -332,15 +346,23 @@ def _start_session(self, **kwargs) -> ClientSession:
332346
if self._client is None:
333347
raise OperationalError("No active connection")
334348

335-
session = self._client.start_session(**kwargs)
349+
session = execute_with_retry(
350+
lambda: self._client.start_session(**kwargs),
351+
self._retry_config,
352+
"start MongoDB session",
353+
)
336354
self._session = session
337355
_logger.info("Started new MongoDB session")
338356
return session
339357

340358
def _end_session(self) -> None:
341359
"""End the current session (internal method)"""
342360
if self._session is not None:
343-
self._session.end_session()
361+
execute_with_retry(
362+
lambda: self._session.end_session(),
363+
self._retry_config,
364+
"end MongoDB session",
365+
)
344366
self._session = None
345367
_logger.info("Ended MongoDB session")
346368

@@ -357,7 +379,11 @@ def _start_transaction(self, **kwargs) -> None:
357379
if self._session is None:
358380
raise OperationalError("No active session")
359381

360-
self._session.start_transaction(**kwargs)
382+
execute_with_retry(
383+
lambda: self._session.start_transaction(**kwargs),
384+
self._retry_config,
385+
"start MongoDB transaction",
386+
)
361387
self._in_transaction = True
362388
self._autocommit = False
363389
_logger.info("Started MongoDB transaction")
@@ -370,7 +396,11 @@ def _commit_transaction(self) -> None:
370396
if not self._session.in_transaction:
371397
raise OperationalError("No active transaction to commit")
372398

373-
self._session.commit_transaction()
399+
execute_with_retry(
400+
lambda: self._session.commit_transaction(),
401+
self._retry_config,
402+
"commit MongoDB transaction",
403+
)
374404
self._in_transaction = False
375405
self._autocommit = True
376406
_logger.info("Committed MongoDB transaction")
@@ -383,7 +413,11 @@ def _abort_transaction(self) -> None:
383413
if not self._session.in_transaction:
384414
raise OperationalError("No active transaction to abort")
385415

386-
self._session.abort_transaction()
416+
execute_with_retry(
417+
lambda: self._session.abort_transaction(),
418+
self._retry_config,
419+
"abort MongoDB transaction",
420+
)
387421
self._in_transaction = False
388422
self._autocommit = True
389423
_logger.info("Aborted MongoDB transaction")
@@ -460,7 +494,11 @@ def test_connection(self) -> bool:
460494
"""Test if the connection is alive"""
461495
try:
462496
if self._client:
463-
self._client.admin.command("ping")
497+
execute_with_retry(
498+
lambda: self._client.admin.command("ping"),
499+
self._retry_config,
500+
"connection health check ping",
501+
)
464502
return True
465503
return False
466504
except Exception as e:

pymongosql/cursor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def execute(self: _T, operation: str, parameters: Optional[Any] = None) -> _T:
110110
command_result=result,
111111
execution_plan=execution_plan_for_rs,
112112
database=self.connection.database,
113+
retry_config=self.connection.retry_config,
113114
**self._kwargs,
114115
)
115116
else:
@@ -125,6 +126,7 @@ def execute(self: _T, operation: str, parameters: Optional[Any] = None) -> _T:
125126
},
126127
execution_plan=stub_plan,
127128
database=self.connection.database,
129+
retry_config=self.connection.retry_config,
128130
**self._kwargs,
129131
)
130132
# Store the actual insert result for reference

0 commit comments

Comments
 (0)