dbapi-mongodb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbapi_mongodb-0.1.0/LICENSE +21 -0
- dbapi_mongodb-0.1.0/PKG-INFO +136 -0
- dbapi_mongodb-0.1.0/README.md +122 -0
- dbapi_mongodb-0.1.0/dbapi_mongodb.egg-info/PKG-INFO +136 -0
- dbapi_mongodb-0.1.0/dbapi_mongodb.egg-info/SOURCES.txt +18 -0
- dbapi_mongodb-0.1.0/dbapi_mongodb.egg-info/dependency_links.txt +1 -0
- dbapi_mongodb-0.1.0/dbapi_mongodb.egg-info/requires.txt +6 -0
- dbapi_mongodb-0.1.0/dbapi_mongodb.egg-info/top_level.txt +1 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/__init__.py +13 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/async_dbapi.py +74 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/dbapi.py +493 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/errors.py +28 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/sqlalchemy_dialect.py +57 -0
- dbapi_mongodb-0.1.0/mongo_dbapi/translation.py +1004 -0
- dbapi_mongodb-0.1.0/pyproject.toml +19 -0
- dbapi_mongodb-0.1.0/setup.cfg +4 -0
- dbapi_mongodb-0.1.0/tests/test_async_dbapi.py +72 -0
- dbapi_mongodb-0.1.0/tests/test_dbapi.py +614 -0
- dbapi_mongodb-0.1.0/tests/test_transactions.py +50 -0
- dbapi_mongodb-0.1.0/tests/test_translation.py +153 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Naruhide KITADA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbapi-mongodb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DBAPI-style adapter that translates SQL (JOIN/DDL/aggregations) into MongoDB operations
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pymongo<4.0,>=3.13
|
|
9
|
+
Requires-Dist: sqlglot>=25.7.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# dbapi-mongodb
|
|
16
|
+
|
|
17
|
+
DBAPI-style adapter that lets you execute a limited subset of SQL against MongoDB by translating SQL to Mongo queries. Built on `pymongo` (3.13.x for MongoDB 3.6 compatibility) and `SQLGlot`.
|
|
18
|
+
|
|
19
|
+
Purpose: let existing DB-API / SQLAlchemy Core / FastAPI code treat MongoDB as “just another dialect.”
|
|
20
|
+
|
|
21
|
+
- PyPI package name: `dbapi-mongodb`
|
|
22
|
+
- Module import name: `mongo_dbapi`
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
- DBAPI-like `Connection`/`Cursor`
|
|
26
|
+
- SQL → Mongo: `SELECT/INSERT/UPDATE/DELETE`, `CREATE/DROP TABLE/INDEX` (ASC/DESC, UNIQUE, composite), `WHERE` (comparisons/`AND`/`OR`/`IN`/`BETWEEN`/`LIKE`→`$regex`/`ILIKE`/regex literal), `ORDER BY`, `LIMIT/OFFSET`, INNER/LEFT JOIN (equijoin, composite keys up to 3 hops), `GROUP BY` + aggregates (COUNT/SUM/AVG/MIN/MAX) + `HAVING`, `UNION ALL`, subqueries (`WHERE IN/EXISTS`, `FROM (SELECT ...)`), `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+
|
|
27
|
+
- `%s` positional and `%(name)s` named parameters; unsupported constructs raise Error IDs (e.g. `[mdb][E2]`)
|
|
28
|
+
- Error IDs for common failures: invalid URI, unsupported SQL, unsafe DML without WHERE, parse errors, connection/auth failures
|
|
29
|
+
- DBAPI fields: `rowcount`, `lastrowid`, `description` (column order: explicit order, or alpha for `SELECT *`; JOIN uses left→right)
|
|
30
|
+
- Transactions: `begin/commit/rollback` wrap Mongo sessions; MongoDB 3.6 and other unsupported envs are treated as no-op success
|
|
31
|
+
- Async dialect (thread-pool backed) for Core CRUD/DDL/Index with FastAPI-friendly usage; minimal ORM CRUD for single-table entities (relationships out of scope)
|
|
32
|
+
|
|
33
|
+
- Use cases
|
|
34
|
+
- Swap in Mongo as “another dialect” for existing SQLAlchemy Core–based infra (Engine/Connection + Table/Column)
|
|
35
|
+
- Point existing Core-based batch/report jobs at Mongo data with minimal changes
|
|
36
|
+
- Minimal ORM CRUD for single-table entities (PK → `_id`)
|
|
37
|
+
- Async dialect for FastAPI/async stacks (thread-pool implementation; native async later)
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
- Python 3.10+
|
|
41
|
+
- MongoDB 3.6 (bundled `mongodb-3.6` binary) or later (note: bundled binary is 3.6, so transactions are unsupported)
|
|
42
|
+
- Virtualenv at `.venv` (already present); dependencies are managed via `pyproject.toml`
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
```bash
|
|
46
|
+
pip install dbapi-mongodb
|
|
47
|
+
# (optional) with a virtualenv: python -m venv .venv && . .venv/bin/activate && pip install dbapi-mongodb
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Start local MongoDB (bundled 3.6)
|
|
51
|
+
```bash
|
|
52
|
+
# Default port 27017; override with PORT
|
|
53
|
+
PORT=27018 ./startdb.sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Start local MongoDB 4.4 (replica set, bundled)
|
|
57
|
+
```bash
|
|
58
|
+
# Default port 27019; uses bundled libssl1.1. LD_LIBRARY_PATH is set inside the script for mongod.
|
|
59
|
+
PORT=27019 ./start4xdb.sh
|
|
60
|
+
# Run tests against 4.x
|
|
61
|
+
MONGODB_URI=mongodb://127.0.0.1:27019 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage example
|
|
65
|
+
```python
|
|
66
|
+
from mongo_dbapi import connect
|
|
67
|
+
|
|
68
|
+
conn = connect("mongodb://127.0.0.1:27018", "mongo_dbapi_test")
|
|
69
|
+
cur = conn.cursor()
|
|
70
|
+
cur.execute("INSERT INTO users (id, name) VALUES (%s, %s)", (1, "Alice"))
|
|
71
|
+
|
|
72
|
+
cur.execute("SELECT id, name FROM users WHERE id = %s", (1,))
|
|
73
|
+
print(cur.fetchall()) # [(1, 'Alice')]
|
|
74
|
+
print(cur.rowcount) # 1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Supported SQL
|
|
78
|
+
- Statements: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE/DROP TABLE`, `CREATE/DROP INDEX`
|
|
79
|
+
- WHERE: comparisons (`=`, `<>`, `>`, `<`, `<=`, `>=`), `AND`, `OR`, `IN`, `BETWEEN`, `LIKE` (`%`/`_` → `$regex`), `ILIKE`, regex literal `/.../`
|
|
80
|
+
- JOIN: INNER/LEFT equijoin (composite keys, up to 3 joins)
|
|
81
|
+
- Aggregation: `GROUP BY` with COUNT/SUM/AVG/MIN/MAX and `HAVING`
|
|
82
|
+
- Subqueries: `WHERE IN/EXISTS` and `FROM (SELECT ...)` (non-correlated; executed first)
|
|
83
|
+
- Set ops: `UNION ALL`
|
|
84
|
+
- Window: `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+ (`[mdb][E2]` on earlier versions)
|
|
85
|
+
- ORDER/LIMIT/OFFSET
|
|
86
|
+
- Unsupported: non-equi joins, FULL/RIGHT OUTER, `UNION` (distinct), window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships
|
|
87
|
+
|
|
88
|
+
## SQLAlchemy
|
|
89
|
+
- DBAPI module attributes: `apilevel="2.0"`, `threadsafety=1`, `paramstyle="pyformat"`.
|
|
90
|
+
- Scheme: `mongodb+dbapi://...` dialect provided (sync + async/thread-pool).
|
|
91
|
+
- Scope: Core text()/Table/Column CRUD/DDL/Index、ORM 最小 CRUD(単一テーブル)、JOIN/UNION ALL/HAVING/subquery/ROW_NUMBER を実通信で確認済み。async dialect は Core CRUD/DDL/Index のラップで、ネイティブ async は今後検討。
|
|
92
|
+
|
|
93
|
+
## Async (FastAPI/Core) - beta
|
|
94
|
+
- **Current implementation wraps the sync driver in a thread pool** (native async driver is planned). Provided via `mongo_dbapi.async_dbapi.connect_async`. API mirrors sync Core: awaitable CRUD/DDL/Index, JOIN/UNION ALL/HAVING/IN/EXISTS/FROM subquery.
|
|
95
|
+
- Transactions: effective on MongoDB 4.x+ only; 3.6 is no-op. Be mindful that MongoDB transactions differ from RDBMS in locking/perf; avoid heavy transactional workloads.
|
|
96
|
+
- Window: `ROW_NUMBER` is available on MongoDB 5.x+; earlier versions return `[mdb][E2] Unsupported SQL construct: WINDOW_FUNCTION`.
|
|
97
|
+
- FastAPI example:
|
|
98
|
+
```python
|
|
99
|
+
from fastapi import FastAPI, Depends
|
|
100
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
|
|
101
|
+
from sqlalchemy import text
|
|
102
|
+
|
|
103
|
+
engine = create_async_engine("mongodb+dbapi://127.0.0.1:27019/mongo_dbapi_test")
|
|
104
|
+
app = FastAPI()
|
|
105
|
+
|
|
106
|
+
async def get_conn() -> AsyncConnection:
|
|
107
|
+
async with engine.connect() as conn:
|
|
108
|
+
yield conn
|
|
109
|
+
|
|
110
|
+
@app.get("/users/{user_id}")
|
|
111
|
+
async def get_user(user_id: str, conn: AsyncConnection = Depends(get_conn)):
|
|
112
|
+
rows = await conn.execute(text("SELECT id, name FROM users WHERE id = :id"), {"id": user_id})
|
|
113
|
+
row = rows.fetchone()
|
|
114
|
+
return dict(row) if row else {}
|
|
115
|
+
```
|
|
116
|
+
- Limitations: async ORM/relationship and statement cache are out of scope; heavy concurrency uses a thread pool under the hood.
|
|
117
|
+
|
|
118
|
+
## Support levels
|
|
119
|
+
- Tested/stable (real Mongo runs): single-collection CRUD, WHERE/ORDER/LIMIT/OFFSET, INNER/LEFT equijoin (up to 3 hops), GROUP BY + aggregates + HAVING, subqueries (WHERE IN/EXISTS, FROM (SELECT ...)), UNION ALL, `ROW_NUMBER()` (MongoDB 5.x+).
|
|
120
|
+
- Not supported / constraints: non-equi JOIN, FULL/RIGHT OUTER, distinct `UNION`, window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships; async is thread-pool based.
|
|
121
|
+
|
|
122
|
+
## Running tests
|
|
123
|
+
```bash
|
|
124
|
+
PORT=27018 ./startdb.sh # if 27017 is taken
|
|
125
|
+
MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
- Transactions on MongoDB 3.6 are treated as no-op; 4.x+ (replica set) uses real sessions and the bundled 4.4 binary passes all tests.
|
|
130
|
+
- Error messages are fixed strings per `docs/spec.md`. Keep logs at DEBUG only (default INFO is silent).
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
MIT License (see `LICENSE`). Provided as-is without warranty; commercial use permitted.
|
|
134
|
+
|
|
135
|
+
## GitHub Sponsors
|
|
136
|
+
Maintained in personal time. If this helps you run MongoDB from DB-API/SQLAlchemy stacks, consider supporting via GitHub Sponsors to keep fixes and version updates coming.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# dbapi-mongodb
|
|
2
|
+
|
|
3
|
+
DBAPI-style adapter that lets you execute a limited subset of SQL against MongoDB by translating SQL to Mongo queries. Built on `pymongo` (3.13.x for MongoDB 3.6 compatibility) and `SQLGlot`.
|
|
4
|
+
|
|
5
|
+
Purpose: let existing DB-API / SQLAlchemy Core / FastAPI code treat MongoDB as “just another dialect.”
|
|
6
|
+
|
|
7
|
+
- PyPI package name: `dbapi-mongodb`
|
|
8
|
+
- Module import name: `mongo_dbapi`
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
- DBAPI-like `Connection`/`Cursor`
|
|
12
|
+
- SQL → Mongo: `SELECT/INSERT/UPDATE/DELETE`, `CREATE/DROP TABLE/INDEX` (ASC/DESC, UNIQUE, composite), `WHERE` (comparisons/`AND`/`OR`/`IN`/`BETWEEN`/`LIKE`→`$regex`/`ILIKE`/regex literal), `ORDER BY`, `LIMIT/OFFSET`, INNER/LEFT JOIN (equijoin, composite keys up to 3 hops), `GROUP BY` + aggregates (COUNT/SUM/AVG/MIN/MAX) + `HAVING`, `UNION ALL`, subqueries (`WHERE IN/EXISTS`, `FROM (SELECT ...)`), `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+
|
|
13
|
+
- `%s` positional and `%(name)s` named parameters; unsupported constructs raise Error IDs (e.g. `[mdb][E2]`)
|
|
14
|
+
- Error IDs for common failures: invalid URI, unsupported SQL, unsafe DML without WHERE, parse errors, connection/auth failures
|
|
15
|
+
- DBAPI fields: `rowcount`, `lastrowid`, `description` (column order: explicit order, or alpha for `SELECT *`; JOIN uses left→right)
|
|
16
|
+
- Transactions: `begin/commit/rollback` wrap Mongo sessions; MongoDB 3.6 and other unsupported envs are treated as no-op success
|
|
17
|
+
- Async dialect (thread-pool backed) for Core CRUD/DDL/Index with FastAPI-friendly usage; minimal ORM CRUD for single-table entities (relationships out of scope)
|
|
18
|
+
|
|
19
|
+
- Use cases
|
|
20
|
+
- Swap in Mongo as “another dialect” for existing SQLAlchemy Core–based infra (Engine/Connection + Table/Column)
|
|
21
|
+
- Point existing Core-based batch/report jobs at Mongo data with minimal changes
|
|
22
|
+
- Minimal ORM CRUD for single-table entities (PK → `_id`)
|
|
23
|
+
- Async dialect for FastAPI/async stacks (thread-pool implementation; native async later)
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
- Python 3.10+
|
|
27
|
+
- MongoDB 3.6 (bundled `mongodb-3.6` binary) or later (note: bundled binary is 3.6, so transactions are unsupported)
|
|
28
|
+
- Virtualenv at `.venv` (already present); dependencies are managed via `pyproject.toml`
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
```bash
|
|
32
|
+
pip install dbapi-mongodb
|
|
33
|
+
# (optional) with a virtualenv: python -m venv .venv && . .venv/bin/activate && pip install dbapi-mongodb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Start local MongoDB (bundled 3.6)
|
|
37
|
+
```bash
|
|
38
|
+
# Default port 27017; override with PORT
|
|
39
|
+
PORT=27018 ./startdb.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Start local MongoDB 4.4 (replica set, bundled)
|
|
43
|
+
```bash
|
|
44
|
+
# Default port 27019; uses bundled libssl1.1. LD_LIBRARY_PATH is set inside the script for mongod.
|
|
45
|
+
PORT=27019 ./start4xdb.sh
|
|
46
|
+
# Run tests against 4.x
|
|
47
|
+
MONGODB_URI=mongodb://127.0.0.1:27019 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage example
|
|
51
|
+
```python
|
|
52
|
+
from mongo_dbapi import connect
|
|
53
|
+
|
|
54
|
+
conn = connect("mongodb://127.0.0.1:27018", "mongo_dbapi_test")
|
|
55
|
+
cur = conn.cursor()
|
|
56
|
+
cur.execute("INSERT INTO users (id, name) VALUES (%s, %s)", (1, "Alice"))
|
|
57
|
+
|
|
58
|
+
cur.execute("SELECT id, name FROM users WHERE id = %s", (1,))
|
|
59
|
+
print(cur.fetchall()) # [(1, 'Alice')]
|
|
60
|
+
print(cur.rowcount) # 1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Supported SQL
|
|
64
|
+
- Statements: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE/DROP TABLE`, `CREATE/DROP INDEX`
|
|
65
|
+
- WHERE: comparisons (`=`, `<>`, `>`, `<`, `<=`, `>=`), `AND`, `OR`, `IN`, `BETWEEN`, `LIKE` (`%`/`_` → `$regex`), `ILIKE`, regex literal `/.../`
|
|
66
|
+
- JOIN: INNER/LEFT equijoin (composite keys, up to 3 joins)
|
|
67
|
+
- Aggregation: `GROUP BY` with COUNT/SUM/AVG/MIN/MAX and `HAVING`
|
|
68
|
+
- Subqueries: `WHERE IN/EXISTS` and `FROM (SELECT ...)` (non-correlated; executed first)
|
|
69
|
+
- Set ops: `UNION ALL`
|
|
70
|
+
- Window: `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+ (`[mdb][E2]` on earlier versions)
|
|
71
|
+
- ORDER/LIMIT/OFFSET
|
|
72
|
+
- Unsupported: non-equi joins, FULL/RIGHT OUTER, `UNION` (distinct), window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships
|
|
73
|
+
|
|
74
|
+
## SQLAlchemy
|
|
75
|
+
- DBAPI module attributes: `apilevel="2.0"`, `threadsafety=1`, `paramstyle="pyformat"`.
|
|
76
|
+
- Scheme: `mongodb+dbapi://...` dialect provided (sync + async/thread-pool).
|
|
77
|
+
- Scope: Core text()/Table/Column CRUD/DDL/Index、ORM 最小 CRUD(単一テーブル)、JOIN/UNION ALL/HAVING/subquery/ROW_NUMBER を実通信で確認済み。async dialect は Core CRUD/DDL/Index のラップで、ネイティブ async は今後検討。
|
|
78
|
+
|
|
79
|
+
## Async (FastAPI/Core) - beta
|
|
80
|
+
- **Current implementation wraps the sync driver in a thread pool** (native async driver is planned). Provided via `mongo_dbapi.async_dbapi.connect_async`. API mirrors sync Core: awaitable CRUD/DDL/Index, JOIN/UNION ALL/HAVING/IN/EXISTS/FROM subquery.
|
|
81
|
+
- Transactions: effective on MongoDB 4.x+ only; 3.6 is no-op. Be mindful that MongoDB transactions differ from RDBMS in locking/perf; avoid heavy transactional workloads.
|
|
82
|
+
- Window: `ROW_NUMBER` is available on MongoDB 5.x+; earlier versions return `[mdb][E2] Unsupported SQL construct: WINDOW_FUNCTION`.
|
|
83
|
+
- FastAPI example:
|
|
84
|
+
```python
|
|
85
|
+
from fastapi import FastAPI, Depends
|
|
86
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
|
|
87
|
+
from sqlalchemy import text
|
|
88
|
+
|
|
89
|
+
engine = create_async_engine("mongodb+dbapi://127.0.0.1:27019/mongo_dbapi_test")
|
|
90
|
+
app = FastAPI()
|
|
91
|
+
|
|
92
|
+
async def get_conn() -> AsyncConnection:
|
|
93
|
+
async with engine.connect() as conn:
|
|
94
|
+
yield conn
|
|
95
|
+
|
|
96
|
+
@app.get("/users/{user_id}")
|
|
97
|
+
async def get_user(user_id: str, conn: AsyncConnection = Depends(get_conn)):
|
|
98
|
+
rows = await conn.execute(text("SELECT id, name FROM users WHERE id = :id"), {"id": user_id})
|
|
99
|
+
row = rows.fetchone()
|
|
100
|
+
return dict(row) if row else {}
|
|
101
|
+
```
|
|
102
|
+
- Limitations: async ORM/relationship and statement cache are out of scope; heavy concurrency uses a thread pool under the hood.
|
|
103
|
+
|
|
104
|
+
## Support levels
|
|
105
|
+
- Tested/stable (real Mongo runs): single-collection CRUD, WHERE/ORDER/LIMIT/OFFSET, INNER/LEFT equijoin (up to 3 hops), GROUP BY + aggregates + HAVING, subqueries (WHERE IN/EXISTS, FROM (SELECT ...)), UNION ALL, `ROW_NUMBER()` (MongoDB 5.x+).
|
|
106
|
+
- Not supported / constraints: non-equi JOIN, FULL/RIGHT OUTER, distinct `UNION`, window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships; async is thread-pool based.
|
|
107
|
+
|
|
108
|
+
## Running tests
|
|
109
|
+
```bash
|
|
110
|
+
PORT=27018 ./startdb.sh # if 27017 is taken
|
|
111
|
+
MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Notes
|
|
115
|
+
- Transactions on MongoDB 3.6 are treated as no-op; 4.x+ (replica set) uses real sessions and the bundled 4.4 binary passes all tests.
|
|
116
|
+
- Error messages are fixed strings per `docs/spec.md`. Keep logs at DEBUG only (default INFO is silent).
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
MIT License (see `LICENSE`). Provided as-is without warranty; commercial use permitted.
|
|
120
|
+
|
|
121
|
+
## GitHub Sponsors
|
|
122
|
+
Maintained in personal time. If this helps you run MongoDB from DB-API/SQLAlchemy stacks, consider supporting via GitHub Sponsors to keep fixes and version updates coming.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbapi-mongodb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DBAPI-style adapter that translates SQL (JOIN/DDL/aggregations) into MongoDB operations
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pymongo<4.0,>=3.13
|
|
9
|
+
Requires-Dist: sqlglot>=25.7.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# dbapi-mongodb
|
|
16
|
+
|
|
17
|
+
DBAPI-style adapter that lets you execute a limited subset of SQL against MongoDB by translating SQL to Mongo queries. Built on `pymongo` (3.13.x for MongoDB 3.6 compatibility) and `SQLGlot`.
|
|
18
|
+
|
|
19
|
+
Purpose: let existing DB-API / SQLAlchemy Core / FastAPI code treat MongoDB as “just another dialect.”
|
|
20
|
+
|
|
21
|
+
- PyPI package name: `dbapi-mongodb`
|
|
22
|
+
- Module import name: `mongo_dbapi`
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
- DBAPI-like `Connection`/`Cursor`
|
|
26
|
+
- SQL → Mongo: `SELECT/INSERT/UPDATE/DELETE`, `CREATE/DROP TABLE/INDEX` (ASC/DESC, UNIQUE, composite), `WHERE` (comparisons/`AND`/`OR`/`IN`/`BETWEEN`/`LIKE`→`$regex`/`ILIKE`/regex literal), `ORDER BY`, `LIMIT/OFFSET`, INNER/LEFT JOIN (equijoin, composite keys up to 3 hops), `GROUP BY` + aggregates (COUNT/SUM/AVG/MIN/MAX) + `HAVING`, `UNION ALL`, subqueries (`WHERE IN/EXISTS`, `FROM (SELECT ...)`), `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+
|
|
27
|
+
- `%s` positional and `%(name)s` named parameters; unsupported constructs raise Error IDs (e.g. `[mdb][E2]`)
|
|
28
|
+
- Error IDs for common failures: invalid URI, unsupported SQL, unsafe DML without WHERE, parse errors, connection/auth failures
|
|
29
|
+
- DBAPI fields: `rowcount`, `lastrowid`, `description` (column order: explicit order, or alpha for `SELECT *`; JOIN uses left→right)
|
|
30
|
+
- Transactions: `begin/commit/rollback` wrap Mongo sessions; MongoDB 3.6 and other unsupported envs are treated as no-op success
|
|
31
|
+
- Async dialect (thread-pool backed) for Core CRUD/DDL/Index with FastAPI-friendly usage; minimal ORM CRUD for single-table entities (relationships out of scope)
|
|
32
|
+
|
|
33
|
+
- Use cases
|
|
34
|
+
- Swap in Mongo as “another dialect” for existing SQLAlchemy Core–based infra (Engine/Connection + Table/Column)
|
|
35
|
+
- Point existing Core-based batch/report jobs at Mongo data with minimal changes
|
|
36
|
+
- Minimal ORM CRUD for single-table entities (PK → `_id`)
|
|
37
|
+
- Async dialect for FastAPI/async stacks (thread-pool implementation; native async later)
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
- Python 3.10+
|
|
41
|
+
- MongoDB 3.6 (bundled `mongodb-3.6` binary) or later (note: bundled binary is 3.6, so transactions are unsupported)
|
|
42
|
+
- Virtualenv at `.venv` (already present); dependencies are managed via `pyproject.toml`
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
```bash
|
|
46
|
+
pip install dbapi-mongodb
|
|
47
|
+
# (optional) with a virtualenv: python -m venv .venv && . .venv/bin/activate && pip install dbapi-mongodb
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Start local MongoDB (bundled 3.6)
|
|
51
|
+
```bash
|
|
52
|
+
# Default port 27017; override with PORT
|
|
53
|
+
PORT=27018 ./startdb.sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Start local MongoDB 4.4 (replica set, bundled)
|
|
57
|
+
```bash
|
|
58
|
+
# Default port 27019; uses bundled libssl1.1. LD_LIBRARY_PATH is set inside the script for mongod.
|
|
59
|
+
PORT=27019 ./start4xdb.sh
|
|
60
|
+
# Run tests against 4.x
|
|
61
|
+
MONGODB_URI=mongodb://127.0.0.1:27019 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage example
|
|
65
|
+
```python
|
|
66
|
+
from mongo_dbapi import connect
|
|
67
|
+
|
|
68
|
+
conn = connect("mongodb://127.0.0.1:27018", "mongo_dbapi_test")
|
|
69
|
+
cur = conn.cursor()
|
|
70
|
+
cur.execute("INSERT INTO users (id, name) VALUES (%s, %s)", (1, "Alice"))
|
|
71
|
+
|
|
72
|
+
cur.execute("SELECT id, name FROM users WHERE id = %s", (1,))
|
|
73
|
+
print(cur.fetchall()) # [(1, 'Alice')]
|
|
74
|
+
print(cur.rowcount) # 1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Supported SQL
|
|
78
|
+
- Statements: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE/DROP TABLE`, `CREATE/DROP INDEX`
|
|
79
|
+
- WHERE: comparisons (`=`, `<>`, `>`, `<`, `<=`, `>=`), `AND`, `OR`, `IN`, `BETWEEN`, `LIKE` (`%`/`_` → `$regex`), `ILIKE`, regex literal `/.../`
|
|
80
|
+
- JOIN: INNER/LEFT equijoin (composite keys, up to 3 joins)
|
|
81
|
+
- Aggregation: `GROUP BY` with COUNT/SUM/AVG/MIN/MAX and `HAVING`
|
|
82
|
+
- Subqueries: `WHERE IN/EXISTS` and `FROM (SELECT ...)` (non-correlated; executed first)
|
|
83
|
+
- Set ops: `UNION ALL`
|
|
84
|
+
- Window: `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` on MongoDB 5.x+ (`[mdb][E2]` on earlier versions)
|
|
85
|
+
- ORDER/LIMIT/OFFSET
|
|
86
|
+
- Unsupported: non-equi joins, FULL/RIGHT OUTER, `UNION` (distinct), window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships
|
|
87
|
+
|
|
88
|
+
## SQLAlchemy
|
|
89
|
+
- DBAPI module attributes: `apilevel="2.0"`, `threadsafety=1`, `paramstyle="pyformat"`.
|
|
90
|
+
- Scheme: `mongodb+dbapi://...` dialect provided (sync + async/thread-pool).
|
|
91
|
+
- Scope: Core text()/Table/Column CRUD/DDL/Index、ORM 最小 CRUD(単一テーブル)、JOIN/UNION ALL/HAVING/subquery/ROW_NUMBER を実通信で確認済み。async dialect は Core CRUD/DDL/Index のラップで、ネイティブ async は今後検討。
|
|
92
|
+
|
|
93
|
+
## Async (FastAPI/Core) - beta
|
|
94
|
+
- **Current implementation wraps the sync driver in a thread pool** (native async driver is planned). Provided via `mongo_dbapi.async_dbapi.connect_async`. API mirrors sync Core: awaitable CRUD/DDL/Index, JOIN/UNION ALL/HAVING/IN/EXISTS/FROM subquery.
|
|
95
|
+
- Transactions: effective on MongoDB 4.x+ only; 3.6 is no-op. Be mindful that MongoDB transactions differ from RDBMS in locking/perf; avoid heavy transactional workloads.
|
|
96
|
+
- Window: `ROW_NUMBER` is available on MongoDB 5.x+; earlier versions return `[mdb][E2] Unsupported SQL construct: WINDOW_FUNCTION`.
|
|
97
|
+
- FastAPI example:
|
|
98
|
+
```python
|
|
99
|
+
from fastapi import FastAPI, Depends
|
|
100
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
|
|
101
|
+
from sqlalchemy import text
|
|
102
|
+
|
|
103
|
+
engine = create_async_engine("mongodb+dbapi://127.0.0.1:27019/mongo_dbapi_test")
|
|
104
|
+
app = FastAPI()
|
|
105
|
+
|
|
106
|
+
async def get_conn() -> AsyncConnection:
|
|
107
|
+
async with engine.connect() as conn:
|
|
108
|
+
yield conn
|
|
109
|
+
|
|
110
|
+
@app.get("/users/{user_id}")
|
|
111
|
+
async def get_user(user_id: str, conn: AsyncConnection = Depends(get_conn)):
|
|
112
|
+
rows = await conn.execute(text("SELECT id, name FROM users WHERE id = :id"), {"id": user_id})
|
|
113
|
+
row = rows.fetchone()
|
|
114
|
+
return dict(row) if row else {}
|
|
115
|
+
```
|
|
116
|
+
- Limitations: async ORM/relationship and statement cache are out of scope; heavy concurrency uses a thread pool under the hood.
|
|
117
|
+
|
|
118
|
+
## Support levels
|
|
119
|
+
- Tested/stable (real Mongo runs): single-collection CRUD, WHERE/ORDER/LIMIT/OFFSET, INNER/LEFT equijoin (up to 3 hops), GROUP BY + aggregates + HAVING, subqueries (WHERE IN/EXISTS, FROM (SELECT ...)), UNION ALL, `ROW_NUMBER()` (MongoDB 5.x+).
|
|
120
|
+
- Not supported / constraints: non-equi JOIN, FULL/RIGHT OUTER, distinct `UNION`, window functions other than `ROW_NUMBER`, correlated subqueries, ORM relationships; async is thread-pool based.
|
|
121
|
+
|
|
122
|
+
## Running tests
|
|
123
|
+
```bash
|
|
124
|
+
PORT=27018 ./startdb.sh # if 27017 is taken
|
|
125
|
+
MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=mongo_dbapi_test .venv/bin/pytest -q
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
- Transactions on MongoDB 3.6 are treated as no-op; 4.x+ (replica set) uses real sessions and the bundled 4.4 binary passes all tests.
|
|
130
|
+
- Error messages are fixed strings per `docs/spec.md`. Keep logs at DEBUG only (default INFO is silent).
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
MIT License (see `LICENSE`). Provided as-is without warranty; commercial use permitted.
|
|
134
|
+
|
|
135
|
+
## GitHub Sponsors
|
|
136
|
+
Maintained in personal time. If this helps you run MongoDB from DB-API/SQLAlchemy stacks, consider supporting via GitHub Sponsors to keep fixes and version updates coming.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dbapi_mongodb.egg-info/PKG-INFO
|
|
5
|
+
dbapi_mongodb.egg-info/SOURCES.txt
|
|
6
|
+
dbapi_mongodb.egg-info/dependency_links.txt
|
|
7
|
+
dbapi_mongodb.egg-info/requires.txt
|
|
8
|
+
dbapi_mongodb.egg-info/top_level.txt
|
|
9
|
+
mongo_dbapi/__init__.py
|
|
10
|
+
mongo_dbapi/async_dbapi.py
|
|
11
|
+
mongo_dbapi/dbapi.py
|
|
12
|
+
mongo_dbapi/errors.py
|
|
13
|
+
mongo_dbapi/sqlalchemy_dialect.py
|
|
14
|
+
mongo_dbapi/translation.py
|
|
15
|
+
tests/test_async_dbapi.py
|
|
16
|
+
tests/test_dbapi.py
|
|
17
|
+
tests/test_transactions.py
|
|
18
|
+
tests/test_translation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mongo_dbapi
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .dbapi import Connection, Cursor, connect
|
|
2
|
+
from .errors import MongoDbApiError
|
|
3
|
+
|
|
4
|
+
# DB-API style exception hook for SQLAlchemy / DB-API 仕様の Error エイリアス
|
|
5
|
+
Error = MongoDbApiError
|
|
6
|
+
|
|
7
|
+
apilevel = "2.0"
|
|
8
|
+
threadsafety = 1
|
|
9
|
+
paramstyle = "pyformat"
|
|
10
|
+
|
|
11
|
+
from . import sqlalchemy_dialect # noqa: E402,F401 register dialect
|
|
12
|
+
|
|
13
|
+
__all__ = ["connect", "Connection", "Cursor", "MongoDbApiError", "Error", "apilevel", "threadsafety", "paramstyle"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from .dbapi import Connection, Cursor, connect
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncCursor:
|
|
10
|
+
"""Async wrapper for Cursor / Cursor の非同期ラッパー"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, sync_cursor: Cursor):
|
|
13
|
+
self._sync_cursor = sync_cursor
|
|
14
|
+
|
|
15
|
+
async def execute(self, sql: str, params: Sequence | Mapping | None = None) -> "AsyncCursor":
|
|
16
|
+
await asyncio.to_thread(self._sync_cursor.execute, sql, params)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
async def executemany(self, sql: str, seq_of_params: Sequence[Sequence | Mapping]) -> "AsyncCursor":
|
|
20
|
+
await asyncio.to_thread(self._sync_cursor.executemany, sql, seq_of_params)
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
async def fetchone(self) -> tuple | None:
|
|
24
|
+
return await asyncio.to_thread(self._sync_cursor.fetchone)
|
|
25
|
+
|
|
26
|
+
async def fetchall(self) -> list[tuple]:
|
|
27
|
+
return await asyncio.to_thread(self._sync_cursor.fetchall)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def rowcount(self) -> int:
|
|
31
|
+
return self._sync_cursor.rowcount
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def lastrowid(self) -> Any:
|
|
35
|
+
return self._sync_cursor.lastrowid
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def description(self):
|
|
39
|
+
return self._sync_cursor.description
|
|
40
|
+
|
|
41
|
+
async def close(self) -> None:
|
|
42
|
+
await asyncio.to_thread(self._sync_cursor.close)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AsyncConnection:
|
|
46
|
+
"""Async wrapper for Connection / Connection の非同期ラッパー"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, sync_conn: Connection):
|
|
49
|
+
self._sync_conn = sync_conn
|
|
50
|
+
|
|
51
|
+
async def cursor(self) -> AsyncCursor:
|
|
52
|
+
cur = await asyncio.to_thread(self._sync_conn.cursor)
|
|
53
|
+
return AsyncCursor(cur)
|
|
54
|
+
|
|
55
|
+
async def begin(self) -> None:
|
|
56
|
+
await asyncio.to_thread(self._sync_conn.begin)
|
|
57
|
+
|
|
58
|
+
async def commit(self) -> None:
|
|
59
|
+
await asyncio.to_thread(self._sync_conn.commit)
|
|
60
|
+
|
|
61
|
+
async def rollback(self) -> None:
|
|
62
|
+
await asyncio.to_thread(self._sync_conn.rollback)
|
|
63
|
+
|
|
64
|
+
async def list_tables(self) -> list[str]:
|
|
65
|
+
return await asyncio.to_thread(self._sync_conn.list_tables)
|
|
66
|
+
|
|
67
|
+
async def close(self) -> None:
|
|
68
|
+
await asyncio.to_thread(self._sync_conn.close)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def connect_async(uri: str, db_name: str) -> AsyncConnection:
|
|
72
|
+
"""Async factory for Connection / Connection の非同期ファクトリ"""
|
|
73
|
+
sync_conn = await asyncio.to_thread(connect, uri, db_name)
|
|
74
|
+
return AsyncConnection(sync_conn)
|