Skip to content

Commit 9f68eb5

Browse files
author
Peng Ren
committed
Update connection string pattern
1 parent f0738f4 commit 9f68eb5

7 files changed

Lines changed: 135 additions & 157 deletions

File tree

pymongosql/__init__.py

Lines changed: 1 addition & 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.2.3"
9+
__version__: str = "0.2.4"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"

pymongosql/connection.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@ def __init__(
3838
3939
Supports connection string patterns:
4040
- mongodb://host:port/database - Core driver (no subquery support)
41-
- mongodb+superset://host:port/database - Superset driver with subquery support
41+
- mongodb+srv://host:port/database - Cloud/SRV connection string
42+
- mongodb://host:port/database?mode=superset - Superset driver with subquery support
43+
- mongodb+srv://host:port/database?mode=superset - Cloud SRV with superset mode
44+
45+
Mode is specified via the ?mode= query parameter. If not specified, defaults to "standard".
4246
4347
See PyMongo MongoClient documentation for full parameter details.
4448
https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/mongoclient/
4549
"""
4650
# Check if connection string specifies mode
4751
connection_string = host if isinstance(host, str) else None
48-
mode, host = ConnectionHelper.parse_connection_string(connection_string)
52+
mode, db_from_uri, host = ConnectionHelper.parse_connection_string(connection_string)
4953

5054
self._mode = kwargs.pop("mode", None)
5155
if not self._mode and mode:
@@ -56,7 +60,10 @@ def __init__(
5660
self._port = port or 27017
5761

5862
# Handle database parameter separately (not a MongoClient parameter)
63+
# Explicit 'database' parameter takes precedence over database in URI
5964
self._database_name = kwargs.pop("database", None)
65+
if not self._database_name and db_from_uri:
66+
self._database_name = db_from_uri
6067

6168
# Store all PyMongo parameters to pass through directly
6269
self._pymongo_params = kwargs.copy()

pymongosql/helper.py

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import logging
99
from typing import Optional, Tuple
10-
from urllib.parse import urlparse
10+
from urllib.parse import parse_qs, urlparse
1111

1212
_logger = logging.getLogger(__name__)
1313

@@ -17,52 +17,80 @@ class ConnectionHelper:
1717
1818
Supports connection string patterns:
1919
- mongodb://host:port/database - Core driver (no subquery support)
20-
- mongodb+superset://host:port/database - Superset driver with subquery support
20+
- mongodb+srv://host:port/database - Cloud/SRV connection string
21+
- mongodb://host:port/database?mode=superset - Superset driver with subquery support
22+
- mongodb+srv://host:port/database?mode=superset - Cloud SRV with superset mode
23+
24+
Mode is specified via query parameter (?mode=superset) and defaults to "standard" if not specified.
2125
"""
2226

2327
@staticmethod
24-
def parse_connection_string(connection_string: str) -> Tuple[str, str, Optional[str], int, Optional[str]]:
28+
def parse_connection_string(connection_string: Optional[str]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
2529
"""
26-
Parse PyMongoSQL connection string and determine driver mode.
30+
Parse MongoDB connection string and extract driver mode from query parameters.
31+
32+
Mode is extracted from the 'mode' query parameter and removed from the normalized
33+
connection string. Database name is extracted from the path. If mode is not specified,
34+
it defaults to "standard".
35+
36+
Supports all standard MongoDB connection string patterns:
37+
mongodb://[username:password@]host1[:port1][,host2[:port2]...][/[defaultauthdb]?options]
38+
39+
Args:
40+
connection_string: MongoDB connection string
41+
42+
Returns:
43+
Tuple of (mode, database_name, normalized_connection_string)
44+
- mode: "standard" (default) or other mode values specified via ?mode= parameter
45+
- database_name: extracted database name from path, or None if not specified
46+
- normalized_connection_string: connection string without the mode parameter
2747
"""
2848
try:
2949
if not connection_string:
30-
return "standard", None
50+
return "standard", None, None
3151

3252
parsed = urlparse(connection_string)
33-
scheme = parsed.scheme
3453

3554
if not parsed.scheme:
36-
return "standard", connection_string
37-
38-
base_scheme = "mongodb"
39-
mode = "standard"
40-
41-
# Determine mode from scheme
42-
if "+" in scheme:
43-
base_scheme = scheme.split("+")[0].lower()
44-
mode = scheme.split("+")[-1].lower()
45-
46-
host = parsed.hostname or "localhost"
47-
port = parsed.port or 27017
48-
database = parsed.path.lstrip("/") if parsed.path else None
49-
50-
# Build normalized connection string with mongodb scheme (removing any +mode)
51-
# Reconstruct netloc with credentials if present
52-
netloc = host
53-
if parsed.username:
54-
creds = parsed.username
55-
if parsed.password:
56-
creds += f":{parsed.password}"
57-
netloc = f"{creds}@{host}"
58-
netloc += f":{port}"
59-
60-
query_part = f"?{parsed.query}" if parsed.query else ""
61-
normalized_connection_string = f"{base_scheme}://{netloc}/{database or ''}{query_part}"
62-
63-
_logger.debug(f"Parsed connection string - Mode: {mode}, Host: {host}, Port: {port}, Database: {database}")
64-
65-
return mode, normalized_connection_string
55+
return "standard", None, connection_string
56+
57+
# Extract mode from query parameters (defaults to "standard" if not specified)
58+
query_params = parse_qs(parsed.query, keep_blank_values=True) if parsed.query else {}
59+
mode = query_params.get("mode", ["standard"])[0]
60+
61+
# Extract database name from path
62+
database_name = None
63+
if parsed.path:
64+
# Remove leading slash and trailing slashes
65+
path_parts = parsed.path.strip("/").split("/")
66+
if path_parts and path_parts[0]: # Get the first path segment as database name
67+
database_name = path_parts[0]
68+
69+
# Remove mode from query parameters
70+
query_params.pop("mode", None)
71+
72+
# Rebuild query string without mode parameter
73+
query_string = (
74+
"&".join(f"{k}={v}" if v else k for k, v_list in query_params.items() for v in v_list)
75+
if query_params
76+
else ""
77+
)
78+
79+
# Reconstruct the connection string without mode parameter
80+
if query_string:
81+
if parsed.path:
82+
normalized_connection_string = f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{query_string}"
83+
else:
84+
normalized_connection_string = f"{parsed.scheme}://{parsed.netloc}?{query_string}"
85+
else:
86+
if parsed.path:
87+
normalized_connection_string = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
88+
else:
89+
normalized_connection_string = f"{parsed.scheme}://{parsed.netloc}"
90+
91+
_logger.debug(f"Parsed connection string - Mode: {mode}, Database: {database_name}")
92+
93+
return mode, database_name, normalized_connection_string
6694

6795
except Exception as e:
6896
_logger.error(f"Failed to parse connection string: {e}")

pymongosql/sqlalchemy_mongodb/__init__.py

Lines changed: 6 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -52,100 +52,23 @@ def create_engine_url(
5252
>>> url = create_engine_url("localhost", 27017, "mydb", mode="superset")
5353
>>> engine = sqlalchemy.create_engine(url)
5454
"""
55-
scheme = "mongodb+superset" if mode == "superset" else "mongodb"
55+
scheme = "mongodb"
5656

5757
params = []
5858
for key, value in kwargs.items():
5959
params.append(f"{key}={value}")
6060

61+
# Add mode parameter if not standard
62+
if mode != "standard":
63+
params.append(f"mode={mode}")
64+
6165
param_str = "&".join(params)
6266
if param_str:
6367
param_str = "?" + param_str
6468

6569
return f"{scheme}://{host}:{port}/{database}{param_str}"
6670

6771

68-
def create_mongodb_url(mongodb_uri: str) -> str:
69-
"""Convert a standard MongoDB URI to work with PyMongoSQL SQLAlchemy dialect.
70-
71-
Args:
72-
mongodb_uri: Standard MongoDB connection string
73-
(e.g., 'mongodb://localhost:27017/mydb' or 'mongodb+srv://...')
74-
75-
Returns:
76-
SQLAlchemy-compatible URL for PyMongoSQL
77-
78-
Example:
79-
>>> url = create_mongodb_url("mongodb://user:pass@localhost:27017/mydb")
80-
>>> engine = sqlalchemy.create_engine(url)
81-
"""
82-
# Return the MongoDB URI as-is since the dialect now handles MongoDB URLs directly
83-
return mongodb_uri
84-
85-
86-
def create_engine_from_mongodb_uri(mongodb_uri: str, **engine_kwargs):
87-
"""Create a SQLAlchemy engine from any MongoDB connection string.
88-
89-
This function handles mongodb://, mongodb+srv://, and mongodb+superset:// URIs properly.
90-
Use this instead of create_engine() directly for special URI schemes.
91-
92-
Args:
93-
mongodb_uri: MongoDB connection string (supports standard, SRV, and superset modes)
94-
**engine_kwargs: Additional arguments passed to create_engine
95-
96-
Returns:
97-
SQLAlchemy Engine object
98-
99-
Example:
100-
>>> # For SRV records (Atlas/Cloud)
101-
>>> engine = create_engine_from_mongodb_uri("mongodb+srv://user:pass@cluster.net/db")
102-
>>> # For standard MongoDB
103-
>>> engine = create_engine_from_mongodb_uri("mongodb://localhost:27017/mydb")
104-
>>> # For superset mode (with subquery support)
105-
>>> engine = create_engine_from_mongodb_uri("mongodb+superset://localhost:27017/mydb")
106-
"""
107-
try:
108-
from sqlalchemy import create_engine
109-
110-
if mongodb_uri.startswith("mongodb+srv://"):
111-
# For MongoDB+SRV, convert to standard mongodb:// for SQLAlchemy compatibility
112-
# SQLAlchemy doesn't handle the + character in scheme names well
113-
converted_uri = mongodb_uri.replace("mongodb+srv://", "mongodb://")
114-
115-
# Create engine with converted URI
116-
engine = create_engine(converted_uri, **engine_kwargs)
117-
118-
def custom_create_connect_args(url):
119-
# Use original SRV URI for actual MongoDB connection
120-
opts = {"host": mongodb_uri}
121-
return [], opts
122-
123-
engine.dialect.create_connect_args = custom_create_connect_args
124-
return engine
125-
elif mongodb_uri.startswith("mongodb+superset://"):
126-
# For MongoDB+Superset, convert to standard mongodb:// for SQLAlchemy compatibility
127-
# but preserve the superset mode by passing it through connection options
128-
converted_uri = mongodb_uri.replace("mongodb+superset://", "mongodb://")
129-
130-
# Create engine with converted URI
131-
engine = create_engine(converted_uri, **engine_kwargs)
132-
133-
def custom_create_connect_args(url):
134-
# Use original superset URI for actual MongoDB connection
135-
# This preserves the superset mode for subquery support
136-
opts = {"host": mongodb_uri}
137-
return [], opts
138-
139-
engine.dialect.create_connect_args = custom_create_connect_args
140-
return engine
141-
else:
142-
# Standard mongodb:// URLs work fine with SQLAlchemy
143-
return create_engine(mongodb_uri, **engine_kwargs)
144-
145-
except ImportError:
146-
raise ImportError("SQLAlchemy is required for engine creation")
147-
148-
14972
def register_dialect():
15073
"""Register the PyMongoSQL dialect with SQLAlchemy.
15174
@@ -166,20 +89,7 @@ def register_dialect():
16689
registry.register("mongodb+srv", "pymongosql.sqlalchemy_mongodb.sqlalchemy_dialect", "PyMongoSQLDialect")
16790
registry.register("mongodb.srv", "pymongosql.sqlalchemy_mongodb.sqlalchemy_dialect", "PyMongoSQLDialect")
16891
except Exception:
169-
# If registration fails we fall back to handling SRV URIs in
170-
# create_engine_from_mongodb_uri by converting 'mongodb+srv' to 'mongodb'.
171-
pass
172-
173-
try:
174-
registry.register(
175-
"mongodb+superset", "pymongosql.sqlalchemy_mongodb.sqlalchemy_dialect", "PyMongoSQLDialect"
176-
)
177-
registry.register(
178-
"mongodb.superset", "pymongosql.sqlalchemy_mongodb.sqlalchemy_dialect", "PyMongoSQLDialect"
179-
)
180-
except Exception:
181-
# If registration fails we fall back to handling Superset URIs in
182-
# create_engine_from_mongodb_uri by converting 'mongodb+superset' to 'mongodb'.
92+
# If registration fails, users can convert URIs to standard mongodb:// format
18393
pass
18494

18595
return True
@@ -197,8 +107,6 @@ def register_dialect():
197107
# Export all SQLAlchemy-related functionality
198108
__all__ = [
199109
"create_engine_url",
200-
"create_mongodb_url",
201-
"create_engine_from_mongodb_uri",
202110
"register_dialect",
203111
"__sqlalchemy_version__",
204112
"__supports_sqlalchemy__",

tests/conftest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ def make_conn(**kwargs):
2828
def make_superset_conn(**kwargs):
2929
"""Create a superset-mode Connection using TEST_URI if provided, otherwise use a local default."""
3030
if TEST_URI:
31-
# Convert test URI to superset mode by replacing mongodb:// with mongodb+superset://
32-
superset_uri = TEST_URI.replace("mongodb://", "mongodb+superset://", 1)
31+
# Convert test URI to superset mode by adding ?mode=superset query parameter
32+
if "?" in TEST_URI:
33+
superset_uri = TEST_URI + "&mode=superset"
34+
else:
35+
superset_uri = TEST_URI + "?mode=superset"
3336
if "database" not in kwargs:
3437
kwargs["database"] = TEST_DB
3538
return Connection(host=superset_uri, **kwargs)
3639

3740
# Default local connection parameters with superset mode
3841
defaults = {
39-
"host": "mongodb+superset://testuser:testpass@localhost:27017/test_db?authSource=test_db",
42+
"host": "mongodb://testuser:testpass@localhost:27017/test_db?authSource=test_db&mode=superset",
4043
"database": "test_db",
4144
}
4245
for k, v in defaults.items():

tests/test_connection.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,16 @@ def test_no_database_param_uses_client_default_database(self):
172172
assert conn.database is not None
173173
assert conn.database.name == "test_db"
174174
conn.close()
175+
176+
def test_connection_string_with_mode_query_param(self):
177+
"""Test that connection string with ?mode parameter is parsed correctly"""
178+
if TEST_URI:
179+
# Test with mode parameter in query string
180+
test_url = f"{TEST_URI.rstrip('/')}/test_db?mode=superset"
181+
else:
182+
test_url = "mongodb://localhost:27017/test_db?mode=superset"
183+
184+
conn = Connection(host=test_url)
185+
assert conn.mode == "superset"
186+
assert conn.database_name == "test_db"
187+
conn.close()

0 commit comments

Comments
 (0)