Skip to content

Commit 09d32de

Browse files
committed
Enhance embedded dialect support: update connection argument handling, add legacy URL support, and implement tests for namespace validation
1 parent 2878af6 commit 09d32de

6 files changed

Lines changed: 113 additions & 15 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ To use with Python Embedded mode through iris-embedded-python-wrapper, when run
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

sqlalchemy_iris/embedded.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ def import_dbapi(cls):
4646
return iris.dbapi
4747

4848
def create_connect_args(self, url):
49-
if url.host or url.port or url.username or url.password:
49+
if url.port or url.username or url.password:
5050
raise exc.ArgumentError(
5151
"iris+emb:// URLs are local-only; use iris:// or iris+intersystems:// "
5252
"for host, port, username, or password connections"
5353
)
5454

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+
5561
supported_query_args = {"path"}
5662
unsupported_query_args = set(url.query).difference(supported_query_args)
5763
if unsupported_query_args:
@@ -62,7 +68,7 @@ def create_connect_args(self, url):
6268

6369
opts = {
6470
"mode": "embedded",
65-
"namespace": url.database if url.database else "USER",
71+
"namespace": url.host or url.database or "USER",
6672
}
6773
path = url.query.get("path")
6874
if path is not None:

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: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ class DataRow(iris.irissdk.dbapiDataRow):
88
pass
99

1010
except (AttributeError, ImportError, TypeError):
11-
pass
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/requirements.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,10 @@ def unicode_ddl(self):
948948
"""Target driver must support some degree of non-ascii symbol
949949
names.
950950
"""
951-
return exclusions.open()
951+
return exclusions.skip_if(
952+
lambda config: getattr(config.db.dialect, "embedded", False),
953+
"embedded DBAPI crashes on non-ASCII result column names",
954+
)
952955

953956
@property
954957
def datetime_interval(self):
@@ -964,8 +967,13 @@ def datetime_literals(self):
964967
literal string, e.g. via the TypeEngine.literal_processor() method.
965968
966969
"""
967-
# works stable only on Community driver
968-
return self.community_driver
970+
return exclusions.only_if(
971+
lambda config: (
972+
config.db.dialect.driver != "intersystems"
973+
and not getattr(config.db.dialect, "embedded", False)
974+
),
975+
"datetime literal rendering is not stable on this driver",
976+
)
969977

970978
@property
971979
def datetime(self):

tests/test_embedded.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
3+
from sqlalchemy import exc
4+
from sqlalchemy.engine import make_url
5+
from sqlalchemy.testing import fixtures
6+
7+
from sqlalchemy_iris.embedded import IRISDialect_emb
8+
9+
10+
def _connect_opts(url):
11+
args, opts = IRISDialect_emb().create_connect_args(make_url(url))
12+
assert args == []
13+
return opts
14+
15+
16+
class EmbeddedURLTest(fixtures.TestBase):
17+
@pytest.mark.parametrize(
18+
("url", "namespace"),
19+
[
20+
("iris+emb://", "USER"),
21+
("iris+emb:///", "USER"),
22+
("iris+emb://SAMPLES", "SAMPLES"),
23+
("iris+emb:///SAMPLES", "SAMPLES"),
24+
],
25+
)
26+
def test_embedded_url_namespace(self, url, namespace):
27+
opts = _connect_opts(url)
28+
29+
assert opts["mode"] == "embedded"
30+
assert opts["namespace"] == namespace
31+
32+
def test_embedded_url_namespace_with_path_option(self):
33+
opts = _connect_opts("iris+emb://SAMPLES?path=/opt/iris")
34+
35+
assert opts["namespace"] == "SAMPLES"
36+
assert opts["path"] == "/opt/iris"
37+
38+
@pytest.mark.parametrize(
39+
"url",
40+
[
41+
"iris+emb://localhost:1972/USER",
42+
"iris+emb://user:pass@USER",
43+
],
44+
)
45+
def test_embedded_url_rejects_remote_connection_parts(self, url):
46+
with pytest.raises(exc.ArgumentError, match="local-only"):
47+
_connect_opts(url)
48+
49+
def test_embedded_url_rejects_duplicate_namespace(self):
50+
with pytest.raises(exc.ArgumentError, match="not both"):
51+
_connect_opts("iris+emb://SAMPLES/USER")

0 commit comments

Comments
 (0)