Skip to content

Commit e9bafae

Browse files
committed
Merge branch 'main' of github.com:grongierisc/django-iris
2 parents d27c203 + dcc52a5 commit e9bafae

File tree

15 files changed

+883
-187
lines changed

15 files changed

+883
-187
lines changed

.github/workflows/python-publish.yml

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ jobs:
2020
runs-on: ubuntu-latest
2121

2222
steps:
23-
- uses: actions/checkout@v2
23+
- uses: actions/checkout@v3
2424
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
2525
if: github.event_name == 'push'
2626
- name: Set up Python
27-
uses: actions/setup-python@v2
27+
uses: actions/setup-python@v3
2828
with:
29-
python-version: '3.x'
29+
python-version: '3.10'
3030
- name: Install dependencies
3131
id: set-version
3232
run: |
@@ -35,38 +35,27 @@ jobs:
3535
[ $GITHUB_EVENT_NAME == 'push' ] && VERSION+=b && VERSION+=$(($(git tag -l "*$VERSION*" | cut -db -f2 | sort -n | tail -1)+1))
3636
sed -ie "s/version = .*/version = $VERSION/" setup.cfg
3737
python -m pip install --upgrade pip
38-
pip install build
39-
echo ::set-output name=version::$VERSION
38+
pip install -U pip setuptools
39+
pip install -r requirements-dev.txt
40+
pip install -r requirements.txt
41+
echo version=$VERSION >> $GITHUB_OUTPUT
4042
NAME="django_iris"-${VERSION}-py3-none-any
41-
echo ::set-output name=name::$NAME
43+
echo name=$NAME >> $GITHUB_OUTPUT
4244
- name: Build package
43-
run: python -m build
45+
run: ./scripts/build-dist.sh
4446
- name: Publish package
45-
uses: pypa/gh-action-pypi-publish@release/v1.5
47+
uses: pypa/gh-action-pypi-publish@release/v1
4648
with:
4749
user: __token__
4850
password: ${{ secrets.PYPI_API_TOKEN }}
4951
- name: Create Beta Release
5052
id: create_release
51-
if: github.event_name == 'push'
52-
uses: actions/create-release@v1
53-
env:
54-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
uses: softprops/action-gh-release@v1
5554
with:
56-
tag_name: ${{ steps.set-version.outputs.version }}
57-
release_name: ${{ steps.set-version.outputs.version }}
55+
tag_name: v${{ steps.set-version.outputs.version }}
5856
prerelease: ${{ github.event_name != 'release' }}
59-
- name: Upload Release Asset
60-
id: upload-release-asset
61-
uses: actions/upload-release-asset@v1
62-
env:
63-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64-
with:
65-
upload_url: ${{ github.event_name == 'release' && github.event.release.upload_url || steps.create_release.outputs.upload_url }}
66-
asset_path: dist/${{ steps.set-version.outputs.name }}.whl
67-
asset_name: ${{ steps.set-version.outputs.name }}.whl
68-
asset_content_type: application/zip
69-
- uses: actions/checkout@v2
57+
files: dist/${{ steps.set-version.outputs.name }}.whl
58+
- uses: actions/checkout@v3
7059
if: github.event_name == 'release'
7160
with:
7261
ref: main

django_iris/__init__.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,133 @@
1-
from django.db.models.expressions import Exists
1+
from django.db.models.functions.math import Random, Ln, Log
2+
from django.db.models.functions.datetime import Now
3+
from django.db.models.expressions import Exists, Func, Value, Col, OrderBy
4+
from django.db.models.functions.text import Chr, ConcatPair, StrIndex
5+
from django.db.models.fields import TextField, CharField
26

7+
fn_template = "{fn %(function)s(%(expressions)s)}"
8+
9+
as_fn = [
10+
"ACOS",
11+
"ASIN",
12+
"ATAN",
13+
"ATAN2",
14+
"COS",
15+
"COT",
16+
"EXP",
17+
"LN",
18+
"LOG",
19+
"LOG10",
20+
"PI",
21+
"SIN",
22+
"TAN",
23+
]
24+
25+
def as_intersystems(cls):
26+
def inner(func):
27+
cls.as_intersystems = func
28+
return inner
29+
30+
class Log10(Func):
31+
function = "LOG10"
32+
arity = 1
33+
lookup_name = "log10"
34+
35+
class Convert(Func):
36+
function = "CONVERT"
37+
lookup_name = "convert"
38+
39+
template = "%(function)s(%(db_type)s, %(expressions)s)"
40+
41+
def __init__(self, expression, output_field):
42+
super().__init__(expression, output_field=output_field)
43+
44+
def as_sql(self, compiler, connection, **extra_context):
45+
extra_context["db_type"] = self.output_field.cast_db_type(connection)
46+
return super().as_sql(compiler, connection, **extra_context)
47+
48+
def convert_streams(expressions):
49+
return [
50+
Convert(expression, CharField()) if isinstance(expression, Col) and isinstance(expression.target, TextField) else expression
51+
for expression in expressions
52+
]
53+
54+
@as_intersystems(Exists)
355
def exists_as_intersystems(self, compiler, connection, template=None, **extra_context):
456
template = "(SELECT COUNT(*) FROM (%(subquery)s))"
557
return self.as_sql(compiler, connection, template, **extra_context)
658

7-
Exists.as_intersystems = exists_as_intersystems
59+
@as_intersystems(Chr)
60+
def chr_as_intersystems(self, compiler, connection, **extra_context):
61+
return self.as_sql(compiler, connection, function="CHAR", **extra_context)
62+
63+
@as_intersystems(ConcatPair)
64+
def concat_as_intersystems(self, compiler, connection, **extra_context):
65+
copy = self.copy()
66+
expressions = convert_streams(copy.get_source_expressions())
67+
"""
68+
STRING in IRIS retuns NULL if all NULL arguments, so, just add empty string, to make it always non NULL
69+
"""
70+
copy.set_source_expressions([Value("")]+ expressions)
71+
return super(ConcatPair, copy).as_sql(
72+
compiler,
73+
connection,
74+
function="STRING",
75+
**extra_context,
76+
)
77+
78+
@as_intersystems(StrIndex)
79+
def instr_as_intersystems(self, compiler, connection, **extra_context):
80+
copy = self.copy()
81+
expressions = convert_streams(copy.get_source_expressions())
82+
copy.set_source_expressions(expressions)
83+
return super(StrIndex, copy).as_sql(
84+
compiler,
85+
connection,
86+
**extra_context,
87+
)
88+
89+
@as_intersystems(Random)
90+
def random_as_intersystems(self, compiler, connection, **extra_context):
91+
return self.as_sql(compiler, connection, template="%%TSQL.ZRAND(1e10)", **extra_context)
92+
93+
@as_intersystems(Ln)
94+
def ln_as_intersystems(self, compiler, connection, **extra_context):
95+
return self.as_sql(compiler, connection, function="LOG", template=fn_template, **extra_context)
96+
97+
98+
@as_intersystems(Log)
99+
def log_as_intersystems(self, compiler, connection, **extra_context):
100+
copy = self.copy()
101+
copy.set_source_expressions(
102+
[
103+
Log10(expression)
104+
for expression in copy.get_source_expressions()[::-1]
105+
]
106+
)
107+
return super(Log, copy).as_sql(
108+
compiler,
109+
connection,
110+
arg_joiner=" / ",
111+
template="%(expressions)s",
112+
**extra_context,
113+
)
114+
115+
@as_intersystems(Func)
116+
def func_as_intersystems(self, compiler, connection, **extra_context):
117+
if self.function in as_fn:
118+
return self.as_sql(compiler, connection, template=fn_template, **extra_context)
119+
return self.as_sql(compiler, connection, **extra_context)
120+
121+
@as_intersystems(Now)
122+
def now_as_intersystems(self, compiler, connection, **extra_context):
123+
return self.as_sql(
124+
compiler, connection, template="CURRENT_TIMESTAMP(6)", **extra_context
125+
)
126+
127+
@as_intersystems(OrderBy)
128+
def orderby_as_intersystems(self, compiler, connection, **extra_context):
129+
copy = self.copy()
130+
# IRIS does not support order NULL
131+
copy.nulls_first = copy.nulls_last = False
132+
return copy.as_sql(compiler, connection, **extra_context)
8133

django_iris/base.py

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.db.backends.base.creation import BaseDatabaseCreation
44
from django.core.exceptions import ImproperlyConfigured
55
from django.utils.asyncio import async_unsafe
6+
from django.utils.functional import cached_property
7+
from django.db.utils import DatabaseErrorWrapper
68

79
from .introspection import DatabaseIntrospection
810
from .features import DatabaseFeatures
@@ -12,21 +14,7 @@
1214
from .creation import DatabaseCreation
1315
from .validation import DatabaseValidation
1416

15-
import intersystems_iris as Database
16-
17-
18-
Database.Warning = type("StandardError", (object,), {})
19-
Database.Error = type("StandardError", (object,), {})
20-
21-
Database.InterfaceError = type("Error", (object,), {})
22-
23-
Database.DatabaseError = type("Error", (object,), {})
24-
Database.DataError = type("DatabaseError", (object,), {})
25-
Database.OperationalError = type("DatabaseError", (object,), {})
26-
Database.IntegrityError = type("DatabaseError", (object,), {})
27-
Database.InternalError = type("DatabaseError", (object,), {})
28-
Database.ProgrammingError = type("DatabaseError", (object,), {})
29-
Database.NotSupportedError = type("DatabaseError", (object,), {})
17+
import intersystems_iris.dbapi._DBAPI as Database
3018

3119

3220
def ignore(*args, **kwargs):
@@ -41,8 +29,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
4129
display_name = 'InterSystems IRIS'
4230

4331
data_types = {
44-
'AutoField': 'INTEGER AUTO_INCREMENT',
45-
'BigAutoField': 'BIGINT AUTO_INCREMENT',
32+
'AutoField': 'IDENTITY',
33+
'BigAutoField': 'IDENTITY',
4634
'BinaryField': 'LONG BINARY',
4735
'BooleanField': 'BIT',
4836
'CharField': 'VARCHAR(%(max_length)s)',
@@ -63,7 +51,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
6351
'PositiveIntegerField': 'INTEGER',
6452
'PositiveSmallIntegerField': 'SMALLINT',
6553
'SlugField': 'VARCHAR(%(max_length)s)',
66-
'SmallAutoField': 'SMALLINT AUTO_INCREMENT',
54+
'SmallAutoField': 'IDENTITY',
6755
'SmallIntegerField': 'SMALLINT',
6856
'TextField': 'TEXT',
6957
'TimeField': 'TIME(6)',
@@ -81,20 +69,31 @@ class DatabaseWrapper(BaseDatabaseWrapper):
8169
'gte': '>= %s',
8270
'lt': '< %s',
8371
'lte': '<= %s',
84-
'startswith': "%%%%STARTSWITH %s",
72+
'startswith': "LIKE %s ESCAPE '\\'",
8573
'endswith': "LIKE %s ESCAPE '\\'",
86-
'istartswith': "%%%%STARTSWITH %s",
74+
'istartswith': "LIKE %s ESCAPE '\\'",
8775
'iendswith': "LIKE %s ESCAPE '\\'",
76+
}
77+
78+
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
79+
pattern_ops = {
80+
"contains": "LIKE '%%' || {} || '%%'",
81+
"icontains": "LIKE '%%' || UPPER({}) || '%%'",
82+
"startswith": "LIKE {} || '%%'",
83+
"istartswith": "LIKE UPPER({}) || '%%'",
84+
"endswith": "LIKE '%%' || {}",
85+
"iendswith": "LIKE '%%' || UPPER({})",
8886

8987
}
88+
9089
Database = Database
9190

92-
_commit = ignore
93-
_rollback = ignore
94-
_savepoint = ignore
95-
_savepoint_commit = ignore
96-
_savepoint_rollback = ignore
97-
_set_autocommit = ignore
91+
# _commit = ignore
92+
# _rollback = ignore
93+
# _savepoint = ignore
94+
# _savepoint_commit = ignore
95+
# _savepoint_rollback = ignore
96+
# _set_autocommit = ignore
9897

9998
SchemaEditorClass = DatabaseSchemaEditor
10099

@@ -105,6 +104,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
105104
ops_class = DatabaseOperations
106105
validation_class = DatabaseValidation
107106

107+
_disable_constraint_checking = False
108108

109109
def get_connection_params(self):
110110
settings_dict = self.settings_dict
@@ -134,10 +134,10 @@ def get_connection_params(self):
134134
conn_params['hostname'] = settings_dict['HOST']
135135
if settings_dict['PORT']:
136136
conn_params['port'] = settings_dict['PORT']
137-
if settings_dict['NAME']:
138-
conn_params['namespace'] = settings_dict['NAME']
139137
if 'NAMESPACE' in settings_dict:
140138
conn_params['namespace'] = settings_dict['NAMESPACE']
139+
if settings_dict['NAME']:
140+
conn_params['namespace'] = settings_dict['NAME']
141141

142142
if (
143143
not conn_params['hostname'] or
@@ -158,16 +158,24 @@ def get_connection_params(self):
158158
"Please supply the USER and PASSWORD"
159159
)
160160

161+
conn_params['application_name'] = 'django'
162+
conn_params["autoCommit"] = self.autocommit
161163
return conn_params
162164

165+
def init_connection_state(self):
166+
pass
167+
163168
@async_unsafe
164169
def get_new_connection(self, conn_params):
165170
return Database.connect(**conn_params)
166171

167-
def init_connection_state(self):
168-
cursor = self.connection.cursor()
169-
# cursor.callproc('%SYSTEM_SQL.Util_SetOption', ['SELECTMODE', 1])
170-
# cursor.callproc('%SYSTEM.SQL_SetSelectMode', [1])
172+
def _close(self):
173+
if self.connection is not None:
174+
# Automatically rollbacks anyway
175+
# self.in_atomic_block = False
176+
# self.needs_rollback = False
177+
with self.wrap_database_errors:
178+
return self.connection.close()
171179

172180
@async_unsafe
173181
def create_cursor(self, name=None):
@@ -182,3 +190,29 @@ def is_usable(self):
182190
return False
183191
else:
184192
return True
193+
194+
@cached_property
195+
def wrap_database_errors(self):
196+
"""
197+
Context manager and decorator that re-throws backend-specific database
198+
exceptions using Django's common wrappers.
199+
"""
200+
return DatabaseErrorWrapper(self)
201+
202+
def _set_autocommit(self, autocommit):
203+
with self.wrap_database_errors:
204+
self.connection.setAutoCommit(autocommit)
205+
206+
def disable_constraint_checking(self):
207+
self._disable_constraint_checking = True
208+
return True
209+
210+
def enable_constraint_checking(self):
211+
self._disable_constraint_checking = False
212+
213+
@async_unsafe
214+
def savepoint_commit(self, sid):
215+
"""
216+
IRIS does not have `RELEASE SAVEPOINT`
217+
so, just ignore it
218+
"""

0 commit comments

Comments
 (0)