Skip to content

Commit 35b253f

Browse files
committed
Official driver support
1 parent 539e3e0 commit 35b253f

File tree

10 files changed

+1525
-23
lines changed

10 files changed

+1525
-23
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ will install them if they are not already in place. To install, just:
1313
pip install sqlalchemy-iris
1414
```
1515

16+
Or to use InterSystems official driver support
17+
18+
```shell
19+
pip install sqlalchemy-iris[intersystems]
20+
```
21+
1622
Usage
1723
---
1824

@@ -23,6 +29,20 @@ from sqlalchemy import create_engine
2329
engine = create_engine("iris://_SYSTEM:SYS@localhost:1972/USER")
2430
```
2531

32+
To use with Python Embedded mode, when run next to IRIS
33+
34+
```python
35+
from sqlalchemy import create_engine
36+
engine = create_engine("iris+emb:///USER")
37+
```
38+
39+
To use with InterSystems official driver, does not work in Python Embedded mode
40+
41+
```python
42+
from sqlalchemy import create_engine
43+
engine = create_engine("iris+intersystems://_SYSTEM:SYS@localhost:1972/USER")
44+
```
45+
2646
IRIS Cloud SQL requires SSLContext
2747

2848
```python

setup.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ project_urls =
2828
python_requires = >=3.8
2929
packages = find:
3030

31+
[options.extras_require]
32+
intersystems =
33+
intersystems-irispython==5.1.0
34+
3135
[tool:pytest]
3236
addopts= --tb native -v -r fxX --maxfail=25 -p no:warnings
3337

3438
[db]
3539
default=iris://_SYSTEM:SYS@localhost:1972/USER
3640
iris=iris://_SYSTEM:SYS@localhost:1972/USER
41+
irisintersystems=iris+intersystems://_SYSTEM:SYS@localhost:1972/USER
3742
irisasync=iris+irisasync://_SYSTEM:SYS@localhost:1972/USER
3843
irisemb=iris+emb:///
3944
sqlite=sqlite:///:memory:

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"iris = sqlalchemy_iris.iris:IRISDialect_iris",
1010
"iris.emb = sqlalchemy_iris.embedded:IRISDialect_emb",
1111
"iris.irisasync = sqlalchemy_iris.irisasync:IRISDialect_irisasync",
12+
"iris.intersystems = sqlalchemy_iris.intersystems:IRISDialect_intersystems",
1213
]
1314
},
1415
)

sqlalchemy_iris/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from . import iris
55

66
try:
7-
import alembic
7+
import alembic # noqa
88
except ImportError:
99
pass
1010
else:
11-
from .alembic import IRISImpl
11+
from .alembic import IRISImpl # noqa
1212

1313
from .base import BIGINT
1414
from .base import BIT
@@ -32,6 +32,7 @@
3232
_registry.register("iris.iris", "sqlalchemy_iris.iris", "IRISDialect_iris")
3333
_registry.register("iris.emb", "sqlalchemy_iris.embedded", "IRISDialect_emb")
3434
_registry.register("iris.irisasync", "sqlalchemy_iris.irisasync", "IRISDialect_irisasync")
35+
_registry.register("iris.intersystems", "sqlalchemy_iris.intersystems", "IRISDialect_intersystems")
3536

3637
__all__ = [
3738
"BIGINT",

sqlalchemy_iris/base.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,7 @@ class IRISDialect(default.DefaultDialect):
900900
type_compiler = IRISTypeCompiler
901901
execution_ctx_cls = IRISExecutionContext
902902

903+
update_returning = False
903904
insert_returning = True
904905
insert_executemany_returning = True
905906
insert_executemany_returning_sort_by_parameter_order = True
@@ -1090,7 +1091,10 @@ def do_execute(self, cursor, query, params, context=None):
10901091
if query.endswith(";"):
10911092
query = query[:-1]
10921093
self._debug(query, params)
1093-
cursor.execute(query, params)
1094+
try:
1095+
cursor.execute(query, params)
1096+
except Exception as ex:
1097+
raise ex
10941098

10951099
def do_executemany(self, cursor, query, params, context=None):
10961100
if query.endswith(";"):
+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import re
2+
import pkg_resources
3+
from ..base import IRISDialect
4+
from sqlalchemy import text, util
5+
from ..base import IRISExecutionContext
6+
from . import dbapi
7+
from .dbapi import connect, Cursor
8+
from .cursor import InterSystemsCursorFetchStrategy
9+
from .dbapi import IntegrityError, OperationalError, DatabaseError
10+
11+
12+
def remap_exception(func):
13+
def wrapper(cursor, *args, **kwargs):
14+
attempt = 0
15+
while attempt < 3:
16+
attempt += 1
17+
try:
18+
cursor.sqlcode = 0
19+
return func(cursor, *args, **kwargs)
20+
except RuntimeError as ex:
21+
# [SQLCODE: <-119>:...
22+
message = ex.args[0]
23+
if '<LIST ERROR>' in message:
24+
# just random error happens in the driver, try again
25+
continue
26+
sqlcode = re.findall(r"^\[SQLCODE: <(-\d+)>:", message)
27+
if not sqlcode:
28+
raise Exception(message)
29+
sqlcode = int(sqlcode[0])
30+
if abs(sqlcode) in [108, 119, 121, 122]:
31+
raise IntegrityError(message)
32+
if abs(sqlcode) in [1, 12]:
33+
raise OperationalError(message)
34+
raise DatabaseError(message)
35+
36+
return wrapper
37+
38+
39+
class InterSystemsExecutionContext(IRISExecutionContext):
40+
cursor_fetch_strategy = InterSystemsCursorFetchStrategy()
41+
42+
def create_cursor(self):
43+
cursor = self._dbapi_connection.cursor()
44+
cursor.sqlcode = 0
45+
return cursor
46+
47+
48+
class IRISDialect_intersystems(IRISDialect):
49+
driver = "intersystems"
50+
51+
execution_ctx_cls = InterSystemsExecutionContext
52+
53+
supports_statement_cache = True
54+
55+
supports_cte = False
56+
57+
supports_sane_rowcount = False
58+
supports_sane_multi_rowcount = False
59+
60+
insert_returning = False
61+
insert_executemany_returning = False
62+
63+
logfile = None
64+
65+
def __init__(self, logfile: str = None, **kwargs):
66+
self.logfile = logfile
67+
IRISDialect.__init__(self, **kwargs)
68+
69+
@classmethod
70+
def import_dbapi(cls):
71+
return dbapi
72+
73+
def connect(self, *cargs, **kwarg):
74+
host = kwarg.get("hostname", "localhost")
75+
port = kwarg.get("port", 1972)
76+
namespace = kwarg.get("namespace", "USER")
77+
username = kwarg.get("username", "_SYSTEM")
78+
password = kwarg.get("password", "SYS")
79+
timeout = kwarg.get("timeout", 10)
80+
sharedmemory = kwarg.get("sharedmemory", False)
81+
logfile = kwarg.get("logfile", self.logfile)
82+
sslconfig = kwarg.get("sslconfig", False)
83+
autoCommit = kwarg.get("autoCommit", False)
84+
isolationLevel = kwarg.get("isolationLevel", 1)
85+
return connect(
86+
host,
87+
port,
88+
namespace,
89+
username,
90+
password,
91+
timeout,
92+
sharedmemory,
93+
logfile,
94+
sslconfig,
95+
autoCommit,
96+
isolationLevel,
97+
)
98+
99+
def create_connect_args(self, url):
100+
opts = {}
101+
102+
opts["application_name"] = "sqlalchemy"
103+
opts["host"] = url.host
104+
opts["port"] = int(url.port) if url.port else 1972
105+
opts["namespace"] = url.database if url.database else "USER"
106+
opts["username"] = url.username if url.username else ""
107+
opts["password"] = url.password if url.password else ""
108+
109+
opts["autoCommit"] = False
110+
111+
if opts["host"] and "@" in opts["host"]:
112+
_h = opts["host"].split("@")
113+
opts["password"] += "@" + _h[0 : len(_h) - 1].join("@")
114+
opts["host"] = _h[len(_h) - 1]
115+
116+
return ([], opts)
117+
118+
def _get_server_version_info(self, connection):
119+
# get the wheel version from iris module
120+
try:
121+
return tuple(
122+
map(
123+
int,
124+
pkg_resources.get_distribution(
125+
"intersystems_irispython"
126+
).version.split("."),
127+
)
128+
)
129+
except: # noqa
130+
return None
131+
132+
def _get_option(self, connection, option):
133+
with connection.cursor() as cursor:
134+
cursor.execute("SELECT %SYSTEM_SQL.Util_GetOption(?)", (option,))
135+
row = cursor.fetchone()
136+
if row:
137+
return row[0]
138+
return None
139+
140+
def set_isolation_level(self, connection, level_str):
141+
if level_str == "AUTOCOMMIT":
142+
connection.autocommit = True
143+
else:
144+
connection.autocommit = False
145+
if level_str not in ["READ COMMITTED", "READ VERIFIED"]:
146+
level_str = "READ UNCOMMITTED"
147+
with connection.cursor() as cursor:
148+
cursor.execute("SET TRANSACTION ISOLATION LEVEL " + level_str)
149+
150+
@remap_exception
151+
def do_execute(self, cursor, query, params, context=None):
152+
if query.endswith(";"):
153+
query = query[:-1]
154+
self._debug(query, params)
155+
cursor.execute(query, params)
156+
157+
@remap_exception
158+
def do_executemany(self, cursor, query, params, context=None):
159+
if query.endswith(";"):
160+
query = query[:-1]
161+
self._debug(query, params, many=True)
162+
if params and (len(params[0]) <= 1):
163+
params = [param[0] if len(param) else None for param in params]
164+
cursor.executemany(query, params)
165+
166+
167+
dialect = IRISDialect_intersystems
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Any
2+
from sqlalchemy import CursorResult
3+
from sqlalchemy.engine.cursor import CursorFetchStrategy
4+
from sqlalchemy.engine.interfaces import DBAPICursor
5+
6+
7+
class InterSystemsCursorFetchStrategy(CursorFetchStrategy):
8+
9+
def fetchone(
10+
self,
11+
result: CursorResult[Any],
12+
dbapi_cursor: DBAPICursor,
13+
hard_close: bool = False,
14+
) -> Any:
15+
row = dbapi_cursor.fetchone()
16+
return tuple(row) if row else None
17+

sqlalchemy_iris/intersystems/dbapi.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
try:
2+
import iris
3+
4+
class Cursor(iris.irissdk.dbapiCursor):
5+
pass
6+
7+
class DataRow(iris.irissdk.dbapiDataRow):
8+
pass
9+
10+
except ImportError:
11+
pass
12+
13+
14+
def connect(*args, **kwargs):
15+
return iris.connect(*args, **kwargs)
16+
17+
18+
# globals
19+
apilevel = "2.0"
20+
threadsafety = 0
21+
paramstyle = "qmark"
22+
23+
Binary = bytes
24+
STRING = str
25+
BINARY = bytes
26+
NUMBER = float
27+
ROWID = str
28+
29+
30+
class Error(Exception):
31+
pass
32+
33+
34+
class Warning(Exception):
35+
pass
36+
37+
38+
class InterfaceError(Error):
39+
pass
40+
41+
42+
class DatabaseError(Error):
43+
pass
44+
45+
46+
class InternalError(DatabaseError):
47+
pass
48+
49+
50+
class OperationalError(DatabaseError):
51+
pass
52+
53+
54+
class ProgrammingError(DatabaseError):
55+
pass
56+
57+
58+
class IntegrityError(DatabaseError):
59+
pass
60+
61+
62+
class DataError(DatabaseError):
63+
pass
64+
65+
66+
class NotSupportedError(DatabaseError):
67+
pass

0 commit comments

Comments
 (0)