Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ An InterSystems IRIS dialect for SQLAlchemy.
Pre-requisites
---

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

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

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

```python
from sqlalchemy import create_engine
engine = create_engine("iris+emb:///USER")
engine = create_engine("iris+emb://USER")
```

The legacy path form `iris+emb:///USER` is also supported.

To use with InterSystems official driver, does not work in Python Embedded mode

```python
Expand Down
2 changes: 1 addition & 1 deletion requirements-iris.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
https://github.com/intersystems-community/intersystems-irispython/releases/download/3.9.3/intersystems_iris-3.9.3-py3-none-any.whl
intersystems-irispython~=5.3.2
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
SQLAlchemy>=1.3
intersystems-irispython~=5.3.2
intersystems-irispython~=5.3.2
iris-embedded-python-wrapper>=0.5.23
5 changes: 2 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = sqlalchemy-iris
version = 0.19.2
version = 0.20.0
description = InterSystems IRIS for SQLAlchemy
long_description = file: README.md
url = https://github.com/caretdev/sqlalchemy-iris
Expand Down Expand Up @@ -41,8 +41,7 @@ addopts= --tb native -v -r fxX -p no:warnings
default=iris://_SYSTEM:SYS@localhost:1972/USER
iris=iris://_SYSTEM:SYS@localhost:1972/USER
irisintersystems=iris+intersystems://_SYSTEM:SYS@localhost:1972/USER
# irisasync=iris+irisasync://_SYSTEM:SYS@localhost:1972/USER
# irisemb=iris+emb:///
irisemb=iris+emb:///
sqlite=sqlite:///:memory:

[sqla_testing]
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
install_requires=[
"SQLAlchemy>=1.3",
"intersystems-irispython~=5.3.2",
"iris-embedded-python-wrapper>=0.5.23",
],
entry_points={
"sqlalchemy.dialects": [
# "iris = sqlalchemy_iris.iris:IRISDialect_iris",
# "iris.emb = sqlalchemy_iris.embedded:IRISDialect_emb",
# "iris.irisasync = sqlalchemy_iris.irisasync:IRISDialect_irisasync",
"iris = sqlalchemy_iris.intersystems:IRISDialect_intersystems",
"iris.emb = sqlalchemy_iris.embedded:IRISDialect_emb",
"iris.intersystems = sqlalchemy_iris.intersystems:IRISDialect_intersystems",
]
},
Expand Down
4 changes: 1 addition & 3 deletions sqlalchemy_iris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@

base.dialect = dialect = intersystems_dialect

# _registry.register("iris.iris", "sqlalchemy_iris.iris", "IRISDialect_iris")
# _registry.register("iris.emb", "sqlalchemy_iris.embedded", "IRISDialect_emb")
# _registry.register("iris.irisasync", "sqlalchemy_iris.irisasync", "IRISDialect_irisasync")
_registry.register("iris.iris", "sqlalchemy_iris.intersystems", "IRISDialect_intersystems")
_registry.register("iris.emb", "sqlalchemy_iris.embedded", "IRISDialect_emb")
_registry.register("iris.intersystems", "sqlalchemy_iris.intersystems", "IRISDialect_intersystems")

__all__ = [
Expand Down
139 changes: 132 additions & 7 deletions sqlalchemy_iris/embedded.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,154 @@
from sqlalchemy import exc
from sqlalchemy import util

from .base import IRISDialect


def _parse_version_number(server_version):
server_version = str(server_version).split(" ")[0].split(".")
return tuple([int("".join(filter(str.isdigit, v)) or 0) for v in server_version])


class IRISDialect_emb(IRISDialect):
driver = "emb"

embedded = True

supports_statement_cache = True

insert_returning = False
insert_executemany_returning = False
insert_executemany_returning_sort_by_parameter_order = False

_isolation_lookup = set(
[
"READ UNCOMMITTED",
"READ COMMITTED",
"REPEATABLE READ",
"SERIALIZABLE",
]
)

def _get_option(self, connection, option):
return connection.iris.cls("%SYSTEM.SQL.Util").GetOption(option)
import iris

return iris.cls("%SYSTEM.SQL.Util").GetOption(option)

def _set_option(self, connection, option, value):
return connection.iris.cls("%SYSTEM.SQL.Util").SetOption(option)
import iris

return iris.cls("%SYSTEM.SQL.Util").SetOption(option, value)

@classmethod
def import_dbapi(cls):
import intersystems_iris.dbapi._DBAPI as dbapi
import iris

return iris.dbapi

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

if url.host and url.database:
raise exc.ArgumentError(
"iris+emb:// URLs accept the namespace as either "
"iris+emb://NAMESPACE or iris+emb:///NAMESPACE, not both"
)

supported_query_args = {"path"}
unsupported_query_args = set(url.query).difference(supported_query_args)
if unsupported_query_args:
raise exc.ArgumentError(
"Unsupported iris+emb:// query argument(s): "
+ ", ".join(sorted(unsupported_query_args))
)

opts = {
"mode": "embedded",
"namespace": url.host or url.database or "USER",
}
path = url.query.get("path")
if path is not None:
if isinstance(path, tuple):
path = path[-1]
opts["path"] = path

return dbapi
return ([], opts)

def _get_server_version_info(self, connection):
server_version = connection._dbapi_connection.iris.system.Version.GetNumber()
server_version = server_version.split(".")
return tuple([int("".join(filter(str.isdigit, v))) for v in server_version])
import iris

version_api = getattr(getattr(iris, "system", None), "Version", None)
get_number = getattr(version_api, "GetNumber", None)
if callable(get_number):
return _parse_version_number(get_number())

return _parse_version_number(iris.cls("%SYSTEM.Version").GetNumber())

def on_connect(self):
def on_connect(conn):
try:
with conn.cursor() as cursor:
cursor.execute(
"select vector_cosine(to_vector('1'), to_vector('1'))"
)
cursor.execute("select to_vector('1')")
cursor.fetchone()
self.supports_vectors = True
except Exception:
self.supports_vectors = False

try:
with conn.cursor() as cursor:
cursor.execute("SELECT TOP 1 Name FROM %Dictionary.PropertyDefinition")
cursor.fetchone()
self._dictionary_access = True
except Exception:
self._dictionary_access = False

if not self._dictionary_access:
util.warn(
"""
There are no access to %Dictionary, may be required for some advanced features,
such as Calculated fields, and include columns in indexes
""".replace(
"\n", ""
)
)

return on_connect

def get_isolation_level(self, connection):
if getattr(connection, "autocommit", False):
return "AUTOCOMMIT"

isolation_level = getattr(connection, "isolation_level", None)
if isolation_level:
return isolation_level.upper()

return "READ COMMITTED"

def set_isolation_level(self, connection, level_str):
if level_str == "AUTOCOMMIT":
connection.autocommit = True
else:
connection.autocommit = False
connection.isolation_level = level_str

def do_execute(self, cursor, query, params, context=None):
if query.endswith(";"):
query = query[:-1]
self._debug(query, params)
cursor.execute(query, params)

def do_executemany(self, cursor, query, params, context=None):
if query.endswith(";"):
query = query[:-1]
self._debug(query, params, True)
cursor.executemany(query, params)


dialect = IRISDialect_emb
13 changes: 7 additions & 6 deletions sqlalchemy_iris/intersystems/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from ..base import IRISExecutionContext
from . import dbapi
from .dbapi import connect
from .dbapi import IntegrityError, OperationalError, DatabaseError
from sqlalchemy.engine.cursor import CursorFetchStrategy


Expand All @@ -17,6 +16,7 @@ def wrapper(cursor, *args, **kwargs):
cursor.sqlcode = 0
return func(cursor, *args, **kwargs)
except RuntimeError as ex:
dbapi._sync_exception_classes()
# [SQLCODE: <-119>:...
message = ex.args[0]
if "<LIST ERROR>" in message:
Expand All @@ -27,10 +27,13 @@ def wrapper(cursor, *args, **kwargs):
raise Exception(message)
sqlcode = int(sqlcode[0])
if abs(sqlcode) in [108, 119, 121, 122]:
raise IntegrityError(sqlcode, message)
raise dbapi.IntegrityError(sqlcode, message)
if abs(sqlcode) in [1, 12]:
raise OperationalError(sqlcode, message)
raise DatabaseError(sqlcode, message)
raise dbapi.OperationalError(sqlcode, message)
raise dbapi.DatabaseError(sqlcode, message)
except Exception:
dbapi._sync_exception_classes()
raise

return wrapper

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

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

"""

dialect = IRISDialect_intersystems
38 changes: 34 additions & 4 deletions sqlalchemy_iris/intersystems/dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ class Cursor(iris.irissdk.dbapiCursor):
class DataRow(iris.irissdk.dbapiDataRow):
pass

except ImportError:
pass
except (AttributeError, ImportError, TypeError):
iris = None


def connect(*args, **kwargs):
return iris.connect(*args, **kwargs)
_sync_exception_classes()
try:
return iris.connect(*args, **kwargs)
finally:
_sync_exception_classes()


def createIRIS(*args, **kwargs):
Expand All @@ -30,7 +34,6 @@ def createIRIS(*args, **kwargs):
NUMBER = float
ROWID = str


class Error(Exception):
pass

Expand Down Expand Up @@ -69,3 +72,30 @@ class DataError(DatabaseError):

class NotSupportedError(DatabaseError):
pass


_EXCEPTION_NAMES = (
"Error",
"Warning",
"InterfaceError",
"DatabaseError",
"InternalError",
"OperationalError",
"ProgrammingError",
"IntegrityError",
"DataError",
"NotSupportedError",
)


def _sync_exception_classes():
if iris is None or not hasattr(iris, "dbapi"):
return

for name in _EXCEPTION_NAMES:
cls = getattr(iris.dbapi, name, None)
if cls is not None:
globals()[name] = cls


_sync_exception_classes()
17 changes: 0 additions & 17 deletions sqlalchemy_iris/irisasync.py

This file was deleted.

Loading
Loading