Skip to content

Commit 10bc81e

Browse files
authored
Merge pull request #34 from grongierisc/main
Bring back iris+emb:// with iris-embedded-python-wrapper
2 parents 8a99af5 + 09d32de commit 10bc81e

14 files changed

Lines changed: 318 additions & 65 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ An InterSystems IRIS dialect for SQLAlchemy.
66
Pre-requisites
77
---
88

9-
This dialect requires SQLAlchemy, InterSystems DB-API driver. They are specified as requirements so ``pip``
10-
will install them if they are not already in place. To install, just:
9+
This dialect requires SQLAlchemy, InterSystems DB-API driver, and iris-embedded-python-wrapper. They are
10+
specified as requirements so ``pip`` will install them if they are not already in place. To install, just:
1111

1212
```shell
1313
pip install sqlalchemy-iris
@@ -29,13 +29,15 @@ from sqlalchemy import create_engine
2929
engine = create_engine("iris://_SYSTEM:SYS@localhost:1972/USER")
3030
```
3131

32-
To use with Python Embedded mode, when run next to IRIS
32+
To use with Python Embedded mode through iris-embedded-python-wrapper, when run next to IRIS
3333

3434
```python
3535
from sqlalchemy import create_engine
36-
engine = create_engine("iris+emb:///USER")
36+
engine = create_engine("iris+emb://USER")
3737
```
3838

39+
The legacy path form `iris+emb:///USER` is also supported.
40+
3941
To use with InterSystems official driver, does not work in Python Embedded mode
4042

4143
```python

requirements-iris.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
https://github.com/intersystems-community/intersystems-irispython/releases/download/3.9.3/intersystems_iris-3.9.3-py3-none-any.whl
1+
intersystems-irispython~=5.3.2

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
SQLAlchemy>=1.3
2-
intersystems-irispython~=5.3.2
2+
intersystems-irispython~=5.3.2
3+
iris-embedded-python-wrapper>=0.5.23

setup.cfg

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = sqlalchemy-iris
3-
version = 0.19.2
3+
version = 0.20.0
44
description = InterSystems IRIS for SQLAlchemy
55
long_description = file: README.md
66
url = https://github.com/caretdev/sqlalchemy-iris
@@ -41,8 +41,7 @@ addopts= --tb native -v -r fxX -p no:warnings
4141
default=iris://_SYSTEM:SYS@localhost:1972/USER
4242
iris=iris://_SYSTEM:SYS@localhost:1972/USER
4343
irisintersystems=iris+intersystems://_SYSTEM:SYS@localhost:1972/USER
44-
# irisasync=iris+irisasync://_SYSTEM:SYS@localhost:1972/USER
45-
# irisemb=iris+emb:///
44+
irisemb=iris+emb:///
4645
sqlite=sqlite:///:memory:
4746

4847
[sqla_testing]

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
install_requires=[
55
"SQLAlchemy>=1.3",
66
"intersystems-irispython~=5.3.2",
7+
"iris-embedded-python-wrapper>=0.5.23",
78
],
89
entry_points={
910
"sqlalchemy.dialects": [
10-
# "iris = sqlalchemy_iris.iris:IRISDialect_iris",
11-
# "iris.emb = sqlalchemy_iris.embedded:IRISDialect_emb",
12-
# "iris.irisasync = sqlalchemy_iris.irisasync:IRISDialect_irisasync",
1311
"iris = sqlalchemy_iris.intersystems:IRISDialect_intersystems",
12+
"iris.emb = sqlalchemy_iris.embedded:IRISDialect_emb",
1413
"iris.intersystems = sqlalchemy_iris.intersystems:IRISDialect_intersystems",
1514
]
1615
},

sqlalchemy_iris/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@
3030

3131
base.dialect = dialect = intersystems_dialect
3232

33-
# _registry.register("iris.iris", "sqlalchemy_iris.iris", "IRISDialect_iris")
34-
# _registry.register("iris.emb", "sqlalchemy_iris.embedded", "IRISDialect_emb")
35-
# _registry.register("iris.irisasync", "sqlalchemy_iris.irisasync", "IRISDialect_irisasync")
3633
_registry.register("iris.iris", "sqlalchemy_iris.intersystems", "IRISDialect_intersystems")
34+
_registry.register("iris.emb", "sqlalchemy_iris.embedded", "IRISDialect_emb")
3735
_registry.register("iris.intersystems", "sqlalchemy_iris.intersystems", "IRISDialect_intersystems")
3836

3937
__all__ = [

sqlalchemy_iris/embedded.py

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,154 @@
1+
from sqlalchemy import exc
2+
from sqlalchemy import util
3+
14
from .base import IRISDialect
25

36

7+
def _parse_version_number(server_version):
8+
server_version = str(server_version).split(" ")[0].split(".")
9+
return tuple([int("".join(filter(str.isdigit, v)) or 0) for v in server_version])
10+
11+
412
class IRISDialect_emb(IRISDialect):
513
driver = "emb"
614

715
embedded = True
816

917
supports_statement_cache = True
1018

19+
insert_returning = False
20+
insert_executemany_returning = False
21+
insert_executemany_returning_sort_by_parameter_order = False
22+
23+
_isolation_lookup = set(
24+
[
25+
"READ UNCOMMITTED",
26+
"READ COMMITTED",
27+
"REPEATABLE READ",
28+
"SERIALIZABLE",
29+
]
30+
)
31+
1132
def _get_option(self, connection, option):
12-
return connection.iris.cls("%SYSTEM.SQL.Util").GetOption(option)
33+
import iris
34+
35+
return iris.cls("%SYSTEM.SQL.Util").GetOption(option)
1336

1437
def _set_option(self, connection, option, value):
15-
return connection.iris.cls("%SYSTEM.SQL.Util").SetOption(option)
38+
import iris
39+
40+
return iris.cls("%SYSTEM.SQL.Util").SetOption(option, value)
1641

1742
@classmethod
1843
def import_dbapi(cls):
19-
import intersystems_iris.dbapi._DBAPI as dbapi
44+
import iris
45+
46+
return iris.dbapi
47+
48+
def create_connect_args(self, url):
49+
if url.port or url.username or url.password:
50+
raise exc.ArgumentError(
51+
"iris+emb:// URLs are local-only; use iris:// or iris+intersystems:// "
52+
"for host, port, username, or password connections"
53+
)
54+
55+
if url.host and url.database:
56+
raise exc.ArgumentError(
57+
"iris+emb:// URLs accept the namespace as either "
58+
"iris+emb://NAMESPACE or iris+emb:///NAMESPACE, not both"
59+
)
60+
61+
supported_query_args = {"path"}
62+
unsupported_query_args = set(url.query).difference(supported_query_args)
63+
if unsupported_query_args:
64+
raise exc.ArgumentError(
65+
"Unsupported iris+emb:// query argument(s): "
66+
+ ", ".join(sorted(unsupported_query_args))
67+
)
68+
69+
opts = {
70+
"mode": "embedded",
71+
"namespace": url.host or url.database or "USER",
72+
}
73+
path = url.query.get("path")
74+
if path is not None:
75+
if isinstance(path, tuple):
76+
path = path[-1]
77+
opts["path"] = path
2078

21-
return dbapi
79+
return ([], opts)
2280

2381
def _get_server_version_info(self, connection):
24-
server_version = connection._dbapi_connection.iris.system.Version.GetNumber()
25-
server_version = server_version.split(".")
26-
return tuple([int("".join(filter(str.isdigit, v))) for v in server_version])
82+
import iris
83+
84+
version_api = getattr(getattr(iris, "system", None), "Version", None)
85+
get_number = getattr(version_api, "GetNumber", None)
86+
if callable(get_number):
87+
return _parse_version_number(get_number())
88+
89+
return _parse_version_number(iris.cls("%SYSTEM.Version").GetNumber())
90+
91+
def on_connect(self):
92+
def on_connect(conn):
93+
try:
94+
with conn.cursor() as cursor:
95+
cursor.execute(
96+
"select vector_cosine(to_vector('1'), to_vector('1'))"
97+
)
98+
cursor.execute("select to_vector('1')")
99+
cursor.fetchone()
100+
self.supports_vectors = True
101+
except Exception:
102+
self.supports_vectors = False
103+
104+
try:
105+
with conn.cursor() as cursor:
106+
cursor.execute("SELECT TOP 1 Name FROM %Dictionary.PropertyDefinition")
107+
cursor.fetchone()
108+
self._dictionary_access = True
109+
except Exception:
110+
self._dictionary_access = False
111+
112+
if not self._dictionary_access:
113+
util.warn(
114+
"""
115+
There are no access to %Dictionary, may be required for some advanced features,
116+
such as Calculated fields, and include columns in indexes
117+
""".replace(
118+
"\n", ""
119+
)
120+
)
121+
122+
return on_connect
123+
124+
def get_isolation_level(self, connection):
125+
if getattr(connection, "autocommit", False):
126+
return "AUTOCOMMIT"
127+
128+
isolation_level = getattr(connection, "isolation_level", None)
129+
if isolation_level:
130+
return isolation_level.upper()
131+
132+
return "READ COMMITTED"
133+
134+
def set_isolation_level(self, connection, level_str):
135+
if level_str == "AUTOCOMMIT":
136+
connection.autocommit = True
137+
else:
138+
connection.autocommit = False
139+
connection.isolation_level = level_str
140+
141+
def do_execute(self, cursor, query, params, context=None):
142+
if query.endswith(";"):
143+
query = query[:-1]
144+
self._debug(query, params)
145+
cursor.execute(query, params)
146+
147+
def do_executemany(self, cursor, query, params, context=None):
148+
if query.endswith(";"):
149+
query = query[:-1]
150+
self._debug(query, params, True)
151+
cursor.executemany(query, params)
27152

28153

29154
dialect = IRISDialect_emb

sqlalchemy_iris/intersystems/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from ..base import IRISExecutionContext
55
from . import dbapi
66
from .dbapi import connect
7-
from .dbapi import IntegrityError, OperationalError, DatabaseError
87
from sqlalchemy.engine.cursor import CursorFetchStrategy
98

109

@@ -17,6 +16,7 @@ def wrapper(cursor, *args, **kwargs):
1716
cursor.sqlcode = 0
1817
return func(cursor, *args, **kwargs)
1918
except RuntimeError as ex:
19+
dbapi._sync_exception_classes()
2020
# [SQLCODE: <-119>:...
2121
message = ex.args[0]
2222
if "<LIST ERROR>" in message:
@@ -27,10 +27,13 @@ def wrapper(cursor, *args, **kwargs):
2727
raise Exception(message)
2828
sqlcode = int(sqlcode[0])
2929
if abs(sqlcode) in [108, 119, 121, 122]:
30-
raise IntegrityError(sqlcode, message)
30+
raise dbapi.IntegrityError(sqlcode, message)
3131
if abs(sqlcode) in [1, 12]:
32-
raise OperationalError(sqlcode, message)
33-
raise DatabaseError(sqlcode, message)
32+
raise dbapi.OperationalError(sqlcode, message)
33+
raise dbapi.DatabaseError(sqlcode, message)
34+
except Exception:
35+
dbapi._sync_exception_classes()
36+
raise
3437

3538
return wrapper
3639

@@ -153,7 +156,6 @@ def set_isolation_level(self, connection, level_str):
153156
with connection.cursor() as cursor:
154157
cursor.execute("SET TRANSACTION ISOLATION LEVEL " + level_str)
155158

156-
"""
157159
@remap_exception
158160
def do_execute(self, cursor, query, params, context=None):
159161
if query.endswith(";"):
@@ -170,6 +172,5 @@ def do_executemany(self, cursor, query, params, context=None):
170172
params = [param[0] if len(param) else None for param in params]
171173
cursor.executemany(query, params)
172174

173-
"""
174175

175176
dialect = IRISDialect_intersystems

sqlalchemy_iris/intersystems/dbapi.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ class Cursor(iris.irissdk.dbapiCursor):
77
class DataRow(iris.irissdk.dbapiDataRow):
88
pass
99

10-
except ImportError:
11-
pass
10+
except (AttributeError, ImportError, TypeError):
11+
iris = None
1212

1313

1414
def connect(*args, **kwargs):
15-
return iris.connect(*args, **kwargs)
15+
_sync_exception_classes()
16+
try:
17+
return iris.connect(*args, **kwargs)
18+
finally:
19+
_sync_exception_classes()
1620

1721

1822
def createIRIS(*args, **kwargs):
@@ -30,7 +34,6 @@ def createIRIS(*args, **kwargs):
3034
NUMBER = float
3135
ROWID = str
3236

33-
3437
class Error(Exception):
3538
pass
3639

@@ -69,3 +72,30 @@ class DataError(DatabaseError):
6972

7073
class NotSupportedError(DatabaseError):
7174
pass
75+
76+
77+
_EXCEPTION_NAMES = (
78+
"Error",
79+
"Warning",
80+
"InterfaceError",
81+
"DatabaseError",
82+
"InternalError",
83+
"OperationalError",
84+
"ProgrammingError",
85+
"IntegrityError",
86+
"DataError",
87+
"NotSupportedError",
88+
)
89+
90+
91+
def _sync_exception_classes():
92+
if iris is None or not hasattr(iris, "dbapi"):
93+
return
94+
95+
for name in _EXCEPTION_NAMES:
96+
cls = getattr(iris.dbapi, name, None)
97+
if cls is not None:
98+
globals()[name] = cls
99+
100+
101+
_sync_exception_classes()

sqlalchemy_iris/irisasync.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)