Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pymongosql/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@
def _run_db_command(db: Any, command: Dict[str, Any], connection: Any, operation_name: str) -> Dict[str, Any]:
"""Run a MongoDB command with optional transaction session and retry policy."""
retry_config = getattr(connection, "retry_config", None)
# command() falls back to DEFAULT_CODEC_OPTIONS, ignoring client options like uuidRepresentation
codec_options = db.codec_options

if connection and connection.session and connection.session.in_transaction:
return execute_with_retry(
lambda: db.command(command, session=connection.session),
lambda: db.command(command, session=connection.session, codec_options=codec_options),
retry_config,
operation_name,
)

return execute_with_retry(
lambda: db.command(command),
lambda: db.command(command, codec_options=codec_options),
retry_config,
operation_name,
)
Expand Down
4 changes: 3 additions & 1 deletion pymongosql/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ def _ensure_results_available(self, count: int = 1) -> None:
"getMore": self._cursor_id,
"collection": self._execution_plan.collection,
}
# Decode later batches like the first one, not with DEFAULT_CODEC_OPTIONS
codec_options = self._database.codec_options
result = execute_with_retry(
lambda: self._database.command(getmore_cmd),
lambda: self._database.command(getmore_cmd, codec_options=codec_options),
self._retry_config,
"getMore command",
)
Expand Down
68 changes: 68 additions & 0 deletions tests/test_command_codec_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""Commands must decode with the database's codec options, not DEFAULT_CODEC_OPTIONS."""
import uuid
from unittest.mock import MagicMock

from bson.binary import Binary, UuidRepresentation
from bson.codec_options import CodecOptions

from pymongosql.executor import _run_db_command
from pymongosql.result_set import ResultSet
from pymongosql.sql.query_builder import QueryExecutionPlan

STANDARD_OPTS = CodecOptions(uuid_representation=UuidRepresentation.STANDARD)


def make_db(codec_options=STANDARD_OPTS, command_result=None):
db = MagicMock()
db.codec_options = codec_options
db.command.return_value = command_result if command_result is not None else {"ok": 1}
return db


def test_run_db_command_passes_codec_options():
db = make_db()
connection = MagicMock(retry_config=None, session=None)

_run_db_command(db, {"find": "users"}, connection, "find")

assert db.command.call_args.kwargs["codec_options"] is STANDARD_OPTS


def test_run_db_command_passes_codec_options_in_transaction():
db = make_db()
connection = MagicMock(retry_config=None)
connection.session.in_transaction = True

_run_db_command(db, {"find": "users"}, connection, "find")

kwargs = db.command.call_args.kwargs
assert kwargs["codec_options"] is STANDARD_OPTS
assert kwargs["session"] is connection.session


def test_getmore_passes_codec_options():
"""Ensure a cursor spanning batches decodes every batch the same way"""
db = make_db(
command_result={"cursor": {"id": 0, "nextBatch": [{"_id": 2}]}},
)
plan = QueryExecutionPlan(collection="users", projection_stage={"_id": 1})
result_set = ResultSet(
command_result={"cursor": {"id": 99, "firstBatch": [{"_id": 1}]}},
execution_plan=plan,
database=db,
)

result_set.fetchall()

assert db.command.called, "getMore was never issued"
assert db.command.call_args.kwargs["codec_options"] is STANDARD_OPTS


def test_standard_codec_options_decode_subtype_4_uuid():
"""Check subtype-4 UUIDs decode to uuid.UUID rather than staying Binary"""
value = uuid.UUID("12345678-1234-5678-1234-567812345678")
encoded = Binary.from_uuid(value, UuidRepresentation.STANDARD)

assert isinstance(encoded.as_uuid(UuidRepresentation.STANDARD), uuid.UUID)
assert STANDARD_OPTS.uuid_representation == UuidRepresentation.STANDARD
5 changes: 4 additions & 1 deletion tests/test_retry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import pytest
from bson.codec_options import DEFAULT_CODEC_OPTIONS
from pymongo.errors import ConnectionFailure, NetworkTimeout

from pymongosql.connection import Connection
Expand Down Expand Up @@ -139,7 +140,9 @@ def test_result_set_getmore_retries_transient_failures():
state = {"calls": 0}

class FakeDatabase:
def command(self, command_payload):
codec_options = DEFAULT_CODEC_OPTIONS

def command(self, command_payload, codec_options=None):
state["calls"] += 1
if state["calls"] < 3:
raise NetworkTimeout("temporary getMore timeout")
Expand Down
Loading