Skip to content

Commit 40f4110

Browse files
author
Peng Ren
committed
Added something new
1 parent ea7ddf3 commit 40f4110

File tree

4 files changed

+464
-162
lines changed

4 files changed

+464
-162
lines changed

README.md

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,233 @@
11
# PyMongoSQL
2-
Python DB API 2.0 (PEP 249) client for MongoDB
2+
3+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4+
[![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/)
6+
7+
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.
8+
9+
## Objectives
10+
11+
PyMongoSQL implements the DB API 2.0 interfaces to provide SQL-like access to MongoDB. The project aims to:
12+
13+
- Bridge the gap between SQL and NoSQL by providing SQL query capabilities for MongoDB
14+
- Support standard SQL DQL (Data Query Language) operations including SELECT statements with WHERE, ORDER BY, and LIMIT clauses
15+
- Provide seamless integration with existing Python applications that expect DB API 2.0 compliance
16+
- Enable easy migration from traditional SQL databases to MongoDB
17+
- Support field aliasing and projection mapping for flexible result set handling
18+
- Maintain high performance through direct `db.command()` execution instead of high-level APIs
19+
20+
## Features
21+
22+
- **DB API 2.0 Compliant**: Full compatibility with Python Database API 2.0 specification
23+
- **SQL Query Support**: SELECT statements with WHERE conditions, field selection, and aliases
24+
- **MongoDB Native Integration**: Direct `db.command()` execution for optimal performance
25+
- **Connection String Support**: MongoDB URI format for easy configuration
26+
- **Result Set Handling**: Support for `fetchone()`, `fetchmany()`, and `fetchall()` operations
27+
- **Field Aliasing**: SQL-style field aliases with automatic projection mapping
28+
- **Context Manager Support**: Automatic resource management with `with` statements
29+
- **Transaction Ready**: Architecture designed for future DML operation support (INSERT, UPDATE, DELETE)
30+
31+
## Requirements
32+
33+
- **Python**: 3.9, 3.10, 3.11, 3.12, 3.13+
34+
- **MongoDB**: 4.0+
35+
36+
## Dependencies
37+
38+
- **PyMongo** (MongoDB Python Driver)
39+
- pymongo >= 4.15.0
40+
41+
- **ANTLR4** (SQL Parser Runtime)
42+
- antlr4-python3-runtime >= 4.13.0
43+
44+
## Installation
45+
46+
```bash
47+
pip install pymongosql
48+
```
49+
50+
Or install from source:
51+
52+
```bash
53+
git clone https://github.com/your-username/PyMongoSQL.git
54+
cd PyMongoSQL
55+
pip install -e .
56+
```
57+
58+
## Quick Start
59+
60+
### Basic Usage
61+
62+
```python
63+
from pymongosql import connect
64+
65+
# Connect to MongoDB
66+
connection = connect(
67+
host="mongodb://localhost:27017",
68+
database="test_db"
69+
)
70+
71+
cursor = connection.cursor()
72+
cursor.execute('SELECT name, email FROM users WHERE age > 25')
73+
print(cursor.fetchall())
74+
```
75+
76+
### Using Connection String
77+
78+
```python
79+
from pymongosql import connect
80+
81+
# Connect with authentication
82+
connection = connect(
83+
host="mongodb://username:password@localhost:27017/database?authSource=admin"
84+
)
85+
86+
cursor = connection.cursor()
87+
cursor.execute('SELECT * FROM products WHERE category = ?', ['Electronics'])
88+
89+
for row in cursor:
90+
print(row)
91+
```
92+
93+
### Context Manager Support
94+
95+
```python
96+
from pymongosql import connect
97+
98+
with connect(host="mongodb://localhost:27017", database="mydb") as conn:
99+
with conn.cursor() as cursor:
100+
cursor.execute('SELECT COUNT(*) as total FROM users')
101+
result = cursor.fetchone()
102+
print(f"Total users: {result['total']}")
103+
```
104+
105+
### Field Aliases and Projections
106+
107+
```python
108+
from pymongosql import connect
109+
110+
connection = connect(host="mongodb://localhost:27017", database="ecommerce")
111+
cursor = connection.cursor()
112+
113+
# Use field aliases for cleaner result sets
114+
cursor.execute('''
115+
SELECT
116+
name AS product_name,
117+
price AS cost,
118+
category AS product_type
119+
FROM products
120+
WHERE in_stock = true
121+
ORDER BY price DESC
122+
LIMIT 10
123+
''')
124+
125+
products = cursor.fetchall()
126+
for product in products:
127+
print(f"{product['product_name']}: ${product['cost']}")
128+
```
129+
130+
### Query with Parameters
131+
132+
```python
133+
from pymongosql import connect
134+
135+
connection = connect(host="mongodb://localhost:27017", database="blog")
136+
cursor = connection.cursor()
137+
138+
# Parameterized queries for security
139+
min_age = 18
140+
status = 'active'
141+
142+
cursor.execute('''
143+
SELECT name, email, created_at
144+
FROM users
145+
WHERE age >= ? AND status = ?
146+
''', [min_age, status])
147+
148+
users = cursor.fetchmany(5) # Fetch first 5 results
149+
while users:
150+
for user in users:
151+
print(f"User: {user['name']} ({user['email']})")
152+
users = cursor.fetchmany(5) # Fetch next 5
153+
```
154+
155+
## Supported SQL Features
156+
157+
### SELECT Statements
158+
- Field selection: `SELECT name, age FROM users`
159+
- Wildcards: `SELECT * FROM products`
160+
- Field aliases: `SELECT name AS user_name, age AS user_age FROM users`
161+
162+
### WHERE Clauses
163+
- Equality: `WHERE name = 'John'`
164+
- Comparisons: `WHERE age > 25`, `WHERE price <= 100.0`
165+
- Logical operators: `WHERE age > 18 AND status = 'active'`
166+
167+
### Sorting and Limiting
168+
- ORDER BY: `ORDER BY name ASC, age DESC`
169+
- LIMIT: `LIMIT 10`
170+
- Combined: `ORDER BY created_at DESC LIMIT 5`
171+
172+
## Architecture
173+
174+
PyMongoSQL uses a multi-layer architecture:
175+
176+
1. **SQL Parser**: Built with ANTLR4 for robust SQL parsing
177+
2. **Query Planner**: Converts SQL AST to MongoDB query plans
178+
3. **Command Executor**: Direct `db.command()` execution for performance
179+
4. **Result Processor**: Handles projection mapping and result set iteration
180+
181+
## Connection Options
182+
183+
```python
184+
from pymongosql.connection import Connection
185+
186+
# Basic connection
187+
conn = Connection(host="localhost", port=27017, database="mydb")
188+
189+
# With authentication
190+
conn = Connection(
191+
host="mongodb://user:pass@host:port/db?authSource=admin",
192+
database="mydb"
193+
)
194+
195+
# Connection properties
196+
print(conn.host) # MongoDB connection URL
197+
print(conn.port) # Port number
198+
print(conn.database_name) # Database name
199+
print(conn.is_connected) # Connection status
200+
```
201+
202+
## Error Handling
203+
204+
```python
205+
from pymongosql import connect
206+
from pymongosql.error import ProgrammingError, SqlSyntaxError
207+
208+
try:
209+
connection = connect(host="mongodb://localhost:27017", database="test")
210+
cursor = connection.cursor()
211+
cursor.execute("INVALID SQL SYNTAX")
212+
except SqlSyntaxError as e:
213+
print(f"SQL syntax error: {e}")
214+
except ProgrammingError as e:
215+
print(f"Programming error: {e}")
216+
```
217+
218+
## Development Status
219+
220+
PyMongoSQL is currently focused on DQL (Data Query Language) operations. Future releases will include:
221+
222+
- **DML Operations**: INSERT, UPDATE, DELETE statements
223+
- **Advanced SQL Features**: JOINs, subqueries, aggregations
224+
- **Schema Operations**: CREATE/DROP collection commands
225+
- **Transaction Support**: Multi-document ACID transactions
226+
227+
## Contributing
228+
229+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
230+
231+
## License
232+
233+
PyMongoSQL is distributed under the [MIT license](https://opensource.org/licenses/MIT).

pymongosql/cursor.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -96,56 +96,54 @@ def _parse_sql(self, sql: str) -> QueryPlan:
9696
raise SqlSyntaxError(f"Failed to parse SQL: {e}")
9797

9898
def _execute_query_plan(self, query_plan: QueryPlan) -> None:
99-
"""Execute a QueryPlan against MongoDB"""
99+
"""Execute a QueryPlan against MongoDB using db.command"""
100100
try:
101-
# Get collection
101+
# Get database
102102
if not query_plan.collection:
103103
raise ProgrammingError("No collection specified in query")
104104

105-
collection = self.connection.get_collection(query_plan.collection)
105+
db = self.connection.database
106106

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

110110
# Convert projection stage from alias mapping to MongoDB format
111-
find_projection = None
112111
if query_plan.projection_stage:
113112
# Convert {"field": "alias"} to {"field": 1} for MongoDB
114-
find_projection = {field: 1 for field in query_plan.projection_stage.keys()}
115-
116-
_logger.debug(f"Executing MongoDB query: filter={find_filter}, projection={find_projection}")
117-
118-
# Execute find query
119-
self._mongo_cursor = collection.find(find_filter, find_projection)
113+
find_command["projection"] = {field: 1 for field in query_plan.projection_stage.keys()}
120114

121115
# Apply sort if specified
122116
if query_plan.sort_stage:
123-
sort_spec = [
124-
(field, direction) for sort_dict in query_plan.sort_stage for field, direction in sort_dict.items()
125-
]
126-
self._mongo_cursor = self._mongo_cursor.sort(sort_spec)
117+
sort_spec = {}
118+
for sort_dict in query_plan.sort_stage:
119+
for field, direction in sort_dict.items():
120+
sort_spec[field] = direction
121+
find_command["sort"] = sort_spec
127122

128123
# Apply skip if specified
129124
if query_plan.skip_stage:
130-
self._mongo_cursor = self._mongo_cursor.skip(query_plan.skip_stage)
125+
find_command["skip"] = query_plan.skip_stage
131126

132127
# Apply limit if specified
133128
if query_plan.limit_stage:
134-
self._mongo_cursor = self._mongo_cursor.limit(query_plan.limit_stage)
129+
find_command["limit"] = query_plan.limit_stage
130+
131+
_logger.debug(f"Executing MongoDB command: {find_command}")
132+
133+
# Execute find command directly
134+
result = db.command(find_command)
135135

136-
# Create result set
137-
self._result_set = self._result_set_class(
138-
mongo_cursor=self._mongo_cursor, query_plan=query_plan, **self._kwargs
139-
)
136+
# Create result set from command result
137+
self._result_set = self._result_set_class(command_result=result, query_plan=query_plan, **self._kwargs)
140138

141139
_logger.info(f"Query executed successfully on collection '{query_plan.collection}'")
142140

143141
except PyMongoError as e:
144-
_logger.error(f"MongoDB query execution failed: {e}")
145-
raise DatabaseError(f"Query execution failed: {e}")
142+
_logger.error(f"MongoDB command execution failed: {e}")
143+
raise DatabaseError(f"Command execution failed: {e}")
146144
except Exception as e:
147-
_logger.error(f"Unexpected error during query execution: {e}")
148-
raise OperationalError(f"Query execution error: {e}")
145+
_logger.error(f"Unexpected error during command execution: {e}")
146+
raise OperationalError(f"Command execution error: {e}")
149147

150148
def execute(self: _T, operation: str, parameters: Optional[Dict[str, Any]] = None) -> _T:
151149
"""Execute a SQL statement

0 commit comments

Comments
 (0)