-
-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add sql server support #36
base: main
Are you sure you want to change the base?
Changes from all commits
2ab95cb
6b92ff7
b8c874b
2936ee3
8e5ba51
c021e01
bc3f8fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,5 +42,7 @@ mysql = | |
asyncmy | ||
sqlite = | ||
aiosqlite | ||
sqlserver = | ||
pyodbc | ||
[options.packages.find] | ||
where=src |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any, Dict, Optional, Sequence | ||
|
||
from mayim.sql.sqlserver.query import SQLServerQuery | ||
|
||
from ..executor import SQLExecutor | ||
|
||
try: | ||
import pyodbc # noqa | ||
|
||
SQLSERVER_ENABLED = True | ||
except ModuleNotFoundError: | ||
SQLSERVER_ENABLED = False | ||
|
||
|
||
class SQLServerExecutor(SQLExecutor): | ||
"""Executor for interfacing with a SQL Server database""" | ||
|
||
ENABLED = SQLSERVER_ENABLED | ||
QUERY_CLASS = SQLServerQuery | ||
POSITIONAL_SUB = r"?" | ||
KEYWORD_SUB = r":\2" | ||
|
||
async def _run_sql( | ||
self, | ||
query: str, | ||
name: str = "", | ||
as_list: bool = False, | ||
no_result: bool = False, | ||
posargs: Optional[Sequence[Any]] = None, | ||
params: Optional[Dict[str, Any]] = None, | ||
): | ||
method_name = self._get_method(as_list=as_list) | ||
async with self.pool.connection() as conn: | ||
exec_values = list(posargs) if posargs else params | ||
cursor = conn.execute(query, exec_values or []) | ||
|
||
columns = [column[0] for column in cursor.description] | ||
if no_result: | ||
return None | ||
|
||
raw = getattr(cursor, method_name)() | ||
|
||
if not as_list: | ||
return dict(zip(columns, raw)) | ||
|
||
results = [] | ||
for row in raw: | ||
results.append(dict(zip(columns, row))) | ||
|
||
return results |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from __future__ import annotations | ||
|
||
from contextlib import asynccontextmanager | ||
from typing import Optional | ||
from urllib.parse import parse_qs, urlparse | ||
|
||
from mayim.base.interface import BaseInterface | ||
from mayim.exception import MayimError | ||
|
||
try: | ||
import pyodbc | ||
|
||
SQLSERVER_ENABLED = True | ||
except ModuleNotFoundError: | ||
SQLSERVER_ENABLED = False | ||
|
||
|
||
class SQLServerPool(BaseInterface): | ||
"""Interface for connecting to a SQL Server database""" | ||
|
||
scheme = "mssql+pyodbc" | ||
|
||
def __init__(self, db_path: str): | ||
self._db_path = db_path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this sort of breaks the pattern. The pools already are meant to parse out these strings and assign to properties. This is so that we can safely output a DSN in logs without exposing secrets, etc. It feels like this is a reimplementation. |
||
super().__init__() | ||
|
||
def _setup_pool(self): | ||
if not SQLSERVER_ENABLED: | ||
raise MayimError( | ||
"SQL Server driver not found. Try reinstalling Mayim: " | ||
"pip install mayim[sqlserver]" | ||
) | ||
|
||
def _parse_url(self) -> str: | ||
url = urlparse(self._db_path) | ||
query = parse_qs(url.query) | ||
driver = query.get("DRIVER", "") | ||
conn_string = ( | ||
f"DRIVER={driver};SERVER={url.hostname};PORT={url.port};" | ||
f"DATABASE={url.path.strip('/')};" | ||
f"UID={url.username};PWD={url.password};" | ||
) | ||
return conn_string | ||
|
||
async def open(self): | ||
"""Open connections to the pool""" | ||
conn_string = self._parse_url() | ||
self._db = pyodbc.connect(conn_string) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why |
||
|
||
async def close(self): | ||
"""Close connections to the pool""" | ||
self._db.close() | ||
|
||
@asynccontextmanager | ||
async def connection(self, timeout: Optional[float] = None): | ||
"""Obtain a connection to the database | ||
|
||
Args: | ||
timeout (float, optional): _Not implemented_. Defaults to `None`. | ||
|
||
Returns: | ||
AsyncIterator[Connection]: Iterator that will yield a connection | ||
|
||
Yields: | ||
Iterator[AsyncIterator[Connection]]: A database connection | ||
""" | ||
if not self._db: | ||
await self.open() | ||
|
||
yield self._db | ||
Comment on lines
+67
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there no actual connection object that can be cached and reused? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import re | ||
|
||
from mayim.exception import MayimError | ||
from mayim.sql.query import ParamType, SQLQuery | ||
|
||
|
||
class SQLServerQuery(SQLQuery): | ||
__slots__ = ("name", "text", "param_type") | ||
PATTERN_POSITIONAL_PARAMETER = re.compile(r"\?") | ||
PATTERN_KEYWORD_PARAMETER = re.compile(r"\:[a-z_][a-z0-9_]") | ||
|
||
def __init__(self, name: str, text: str) -> None: | ||
super().__init__(name, text) | ||
positional_argument_exists = bool( | ||
self.PATTERN_POSITIONAL_PARAMETER.search(self.text) | ||
) | ||
keyword_argument_exists = bool( | ||
self.PATTERN_KEYWORD_PARAMETER.search(self.text) | ||
) | ||
if keyword_argument_exists: | ||
raise MayimError("Only Positional arguments allowed in pyODBC") | ||
if positional_argument_exists: | ||
self.param_type = ParamType.POSITIONAL | ||
else: | ||
self.param_type = ParamType.NONE | ||
Comment on lines
+7
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make sure to add some unit tests to this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why
pyodbc
and not aioodbc?