Skip to content

Commit b9e2319

Browse files
passrenPeng Ren
andauthored
0.1.2 (#1)
* enhance Connection * Add trigger for feature branch * Fix code smell * Update readme * Fix test cases * Refactor test cases * Fixed test case * Add more cases for query * Fix code formatting issue * Fixed something bugs --------- Co-authored-by: Peng Ren <ia250@cummins.com>
1 parent 68014cc commit b9e2319

16 files changed

Lines changed: 1075 additions & 837 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI Tests
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [ main, "*.*.*" ]
66
pull_request:
77
branches: [ main ]
88
workflow_call:

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# PyMongoSQL
22

3-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3+
[![Test](https://github.com/passren/PyMongoSQL/actions/workflows/ci.yml/badge.svg)](https://github.com/passren/PyMongoSQL/actions/workflows/ci.yml)
4+
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://github.com/passren/PyMongoSQL/blob/0.1.2/LICENSE)
46
[![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5-
[![MongoDB](https://img.shields.io/badge/MongoDB-4.0+-green.svg)](https://www.mongodb.com/)
7+
[![MongoDB](https://img.shields.io/badge/MongoDB-7.0+-green.svg)](https://www.mongodb.com/)
68

79
PyMongoSQL is a Python [DB API 2.0 (PEP 249)](https://www.python.org/dev/peps/pep-0249/) client for [MongoDB](https://www.mongodb.com/). It provides a familiar SQL interface to MongoDB, allowing developers to use SQL queries to interact with MongoDB collections.
810

pymongosql/connection.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ def __init__(
7878
else:
7979
# Just create the client without testing connection
8080
self._client = MongoClient(**self._pymongo_params)
81-
if self._database_name:
82-
self._database = self._client[self._database_name]
81+
# Initialize the database according to explicit parameter or client's default
82+
self._init_database()
8383

8484
def _connect(self) -> None:
8585
"""Establish connection to MongoDB"""
@@ -91,19 +91,55 @@ def _connect(self) -> None:
9191
# Test connection
9292
self._client.admin.command("ping")
9393

94-
# Set database if specified
95-
if self._database_name:
96-
self._database = self._client[self._database_name]
94+
# Initialize the database according to explicit parameter or client's default
95+
# This may raise OperationalError if no database could be determined; allow it to bubble up
96+
self._init_database()
9797

9898
_logger.info(f"Successfully connected to MongoDB at {self._host}:{self._port}")
9999

100+
except OperationalError:
101+
# Allow OperationalError (e.g., no database selected) to propagate unchanged
102+
raise
100103
except ConnectionFailure as e:
101104
_logger.error(f"Failed to connect to MongoDB: {e}")
102105
raise OperationalError(f"Could not connect to MongoDB: {e}")
103106
except Exception as e:
104107
_logger.error(f"Unexpected error during connection: {e}")
105108
raise DatabaseError(f"Database connection error: {e}")
106109

110+
def _init_database(self) -> None:
111+
"""Internal helper to initialize `self._database`.
112+
113+
Behavior:
114+
- If `database` parameter was provided explicitly, use that database name.
115+
- Otherwise, try to use the MongoClient's default database (from the URI path).
116+
If no default is set, leave `self._database` as None.
117+
"""
118+
if self._client is None:
119+
self._database = None
120+
return
121+
122+
if self._database_name is not None:
123+
# Explicit database parameter takes precedence
124+
try:
125+
self._database = self._client.get_database(self._database_name)
126+
except Exception:
127+
# Fallback to subscription style access
128+
self._database = self._client[self._database_name]
129+
else:
130+
# No explicit database; try to get client's default
131+
try:
132+
self._database = self._client.get_default_database()
133+
except Exception:
134+
# PyMongo can raise various exceptions for missing database
135+
self._database = None
136+
137+
# Enforce that a database must be selected
138+
if self._database is None:
139+
raise OperationalError(
140+
"No database selected. Provide 'database' parameter or include a database in the URI path."
141+
)
142+
107143
@property
108144
def client(self) -> MongoClient:
109145
"""Get the PyMongo client"""

pymongosql/cursor.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# -*- coding: utf-8 -*-
22
import logging
3-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar
3+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, TypeVar
44

55
from pymongo.cursor import Cursor as MongoCursor
66
from pymongo.errors import PyMongoError
77

88
from .common import BaseCursor, CursorIterator
99
from .error import DatabaseError, OperationalError, ProgrammingError, SqlSyntaxError
1010
from .result_set import ResultSet
11-
from .sql.builder import QueryPlan
11+
from .sql.builder import ExecutionPlan
1212
from .sql.parser import SQLParser
1313

1414
if TYPE_CHECKING:
@@ -31,7 +31,7 @@ def __init__(self, connection: "Connection", **kwargs) -> None:
3131
self._kwargs = kwargs
3232
self._result_set: Optional[ResultSet] = None
3333
self._result_set_class = ResultSet
34-
self._current_query_plan: Optional[QueryPlan] = None
34+
self._current_execution_plan: Optional[ExecutionPlan] = None
3535
self._mongo_cursor: Optional[MongoCursor] = None
3636
self._is_closed = False
3737

@@ -78,65 +78,66 @@ def _check_closed(self) -> None:
7878
if self._is_closed:
7979
raise ProgrammingError("Cursor is closed")
8080

81-
def _parse_sql(self, sql: str) -> QueryPlan:
82-
"""Parse SQL statement and return QueryPlan"""
81+
def _parse_sql(self, sql: str) -> ExecutionPlan:
82+
"""Parse SQL statement and return ExecutionPlan"""
8383
try:
8484
parser = SQLParser(sql)
85-
query_plan = parser.get_query_plan()
85+
execution_plan = parser.get_execution_plan()
8686

87-
if not query_plan.validate():
87+
if not execution_plan.validate():
8888
raise SqlSyntaxError("Generated query plan is invalid")
8989

90-
return query_plan
90+
return execution_plan
9191

9292
except SqlSyntaxError:
9393
raise
9494
except Exception as e:
9595
_logger.error(f"SQL parsing failed: {e}")
9696
raise SqlSyntaxError(f"Failed to parse SQL: {e}")
9797

98-
def _execute_query_plan(self, query_plan: QueryPlan) -> None:
99-
"""Execute a QueryPlan against MongoDB using db.command"""
98+
def _execute_execution_plan(self, execution_plan: ExecutionPlan) -> None:
99+
"""Execute an ExecutionPlan against MongoDB using db.command"""
100100
try:
101101
# Get database
102-
if not query_plan.collection:
102+
if not execution_plan.collection:
103103
raise ProgrammingError("No collection specified in query")
104104

105105
db = self.connection.database
106106

107107
# Build MongoDB find command
108-
find_command = {"find": query_plan.collection, "filter": query_plan.filter_stage or {}}
108+
find_command = {"find": execution_plan.collection, "filter": execution_plan.filter_stage or {}}
109109

110-
# Convert projection stage from alias mapping to MongoDB format
111-
if query_plan.projection_stage:
112-
# Convert {"field": "alias"} to {"field": 1} for MongoDB
113-
find_command["projection"] = {field: 1 for field in query_plan.projection_stage.keys()}
110+
# Apply projection if specified (already in MongoDB format)
111+
if execution_plan.projection_stage:
112+
find_command["projection"] = execution_plan.projection_stage
114113

115114
# Apply sort if specified
116-
if query_plan.sort_stage:
115+
if execution_plan.sort_stage:
117116
sort_spec = {}
118-
for sort_dict in query_plan.sort_stage:
117+
for sort_dict in execution_plan.sort_stage:
119118
for field, direction in sort_dict.items():
120119
sort_spec[field] = direction
121120
find_command["sort"] = sort_spec
122121

123122
# Apply skip if specified
124-
if query_plan.skip_stage:
125-
find_command["skip"] = query_plan.skip_stage
123+
if execution_plan.skip_stage:
124+
find_command["skip"] = execution_plan.skip_stage
126125

127126
# Apply limit if specified
128-
if query_plan.limit_stage:
129-
find_command["limit"] = query_plan.limit_stage
127+
if execution_plan.limit_stage:
128+
find_command["limit"] = execution_plan.limit_stage
130129

131130
_logger.debug(f"Executing MongoDB command: {find_command}")
132131

133132
# Execute find command directly
134133
result = db.command(find_command)
135134

136135
# Create result set from command result
137-
self._result_set = self._result_set_class(command_result=result, query_plan=query_plan, **self._kwargs)
136+
self._result_set = self._result_set_class(
137+
command_result=result, execution_plan=execution_plan, **self._kwargs
138+
)
138139

139-
_logger.info(f"Query executed successfully on collection '{query_plan.collection}'")
140+
_logger.info(f"Query executed successfully on collection '{execution_plan.collection}'")
140141

141142
except PyMongoError as e:
142143
_logger.error(f"MongoDB command execution failed: {e}")
@@ -161,11 +162,11 @@ def execute(self: _T, operation: str, parameters: Optional[Dict[str, Any]] = Non
161162
_logger.warning("Parameter substitution not yet implemented, ignoring parameters")
162163

163164
try:
164-
# Parse SQL to QueryPlan
165-
self._current_query_plan = self._parse_sql(operation)
165+
# Parse SQL to ExecutionPlan
166+
self._current_execution_plan = self._parse_sql(operation)
166167

167-
# Execute the query plan
168-
self._execute_query_plan(self._current_query_plan)
168+
# Execute the execution plan
169+
self._execute_execution_plan(self._current_execution_plan)
169170

170171
return self
171172

@@ -205,7 +206,7 @@ def flush(self) -> None:
205206
# For now, this is a no-op
206207
pass
207208

208-
def fetchone(self) -> Optional[Dict[str, Any]]:
209+
def fetchone(self) -> Optional[Sequence[Any]]:
209210
"""Fetch the next row from the result set"""
210211
self._check_closed()
211212

@@ -214,7 +215,7 @@ def fetchone(self) -> Optional[Dict[str, Any]]:
214215

215216
return self._result_set.fetchone()
216217

217-
def fetchmany(self, size: Optional[int] = None) -> List[Dict[str, Any]]:
218+
def fetchmany(self, size: Optional[int] = None) -> List[Sequence[Any]]:
218219
"""Fetch multiple rows from the result set"""
219220
self._check_closed()
220221

@@ -223,7 +224,7 @@ def fetchmany(self, size: Optional[int] = None) -> List[Dict[str, Any]]:
223224

224225
return self._result_set.fetchmany(size)
225226

226-
def fetchall(self) -> List[Dict[str, Any]]:
227+
def fetchall(self) -> List[Sequence[Any]]:
227228
"""Fetch all remaining rows from the result set"""
228229
self._check_closed()
229230

0 commit comments

Comments
 (0)