Skip to content

Commit 6abe81b

Browse files
committed
work in progress, many fixes
1 parent c21fd77 commit 6abe81b

File tree

10 files changed

+825
-158
lines changed

10 files changed

+825
-158
lines changed

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: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
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
8+
from django.db.backends.utils import debug_transaction
69

710
from .introspection import DatabaseIntrospection
811
from .features import DatabaseFeatures
@@ -12,21 +15,7 @@
1215
from .creation import DatabaseCreation
1316
from .validation import DatabaseValidation
1417

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,), {})
18+
import intersystems_iris.dbapi._DBAPI as Database
3019

3120

3221
def ignore(*args, **kwargs):
@@ -41,8 +30,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
4130
display_name = 'InterSystems IRIS'
4231

4332
data_types = {
44-
'AutoField': 'INTEGER AUTO_INCREMENT',
45-
'BigAutoField': 'BIGINT AUTO_INCREMENT',
33+
'AutoField': 'IDENTITY',
34+
'BigAutoField': 'IDENTITY',
4635
'BinaryField': 'LONG BINARY',
4736
'BooleanField': 'BIT',
4837
'CharField': 'VARCHAR(%(max_length)s)',
@@ -63,7 +52,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
6352
'PositiveIntegerField': 'INTEGER',
6453
'PositiveSmallIntegerField': 'SMALLINT',
6554
'SlugField': 'VARCHAR(%(max_length)s)',
66-
'SmallAutoField': 'SMALLINT AUTO_INCREMENT',
55+
'SmallAutoField': 'IDENTITY',
6756
'SmallIntegerField': 'SMALLINT',
6857
'TextField': 'TEXT',
6958
'TimeField': 'TIME(6)',
@@ -81,20 +70,31 @@ class DatabaseWrapper(BaseDatabaseWrapper):
8170
'gte': '>= %s',
8271
'lt': '< %s',
8372
'lte': '<= %s',
84-
'startswith': "%%%%STARTSWITH %s",
73+
'startswith': "LIKE %s ESCAPE '\\'",
8574
'endswith': "LIKE %s ESCAPE '\\'",
86-
'istartswith': "%%%%STARTSWITH %s",
75+
'istartswith': "LIKE %s ESCAPE '\\'",
8776
'iendswith': "LIKE %s ESCAPE '\\'",
77+
}
78+
79+
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
80+
pattern_ops = {
81+
"contains": "LIKE '%%' || {} || '%%'",
82+
"icontains": "LIKE '%%' || UPPER({}) || '%%'",
83+
"startswith": "LIKE {} || '%%'",
84+
"istartswith": "LIKE UPPER({}) || '%%'",
85+
"endswith": "LIKE '%%' || {}",
86+
"iendswith": "LIKE '%%' || UPPER({})",
8887

8988
}
89+
9090
Database = Database
9191

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

9999
SchemaEditorClass = DatabaseSchemaEditor
100100

@@ -105,6 +105,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
105105
ops_class = DatabaseOperations
106106
validation_class = DatabaseValidation
107107

108+
_disable_constraint_checking = False
108109

109110
def get_connection_params(self):
110111
settings_dict = self.settings_dict
@@ -134,10 +135,10 @@ def get_connection_params(self):
134135
conn_params['hostname'] = settings_dict['HOST']
135136
if settings_dict['PORT']:
136137
conn_params['port'] = settings_dict['PORT']
137-
if settings_dict['NAME']:
138-
conn_params['namespace'] = settings_dict['NAME']
139138
if 'NAMESPACE' in settings_dict:
140139
conn_params['namespace'] = settings_dict['NAMESPACE']
140+
if settings_dict['NAME']:
141+
conn_params['namespace'] = settings_dict['NAME']
141142

142143
if (
143144
not conn_params['hostname'] or
@@ -158,16 +159,21 @@ def get_connection_params(self):
158159
"Please supply the USER and PASSWORD"
159160
)
160161

162+
conn_params['application_name'] = 'django'
163+
conn_params["autoCommit"] = self.autocommit
161164
return conn_params
162165

163166
@async_unsafe
164167
def get_new_connection(self, conn_params):
165168
return Database.connect(**conn_params)
166169

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])
170+
def _close(self):
171+
if self.connection is not None:
172+
# Automatically rollbacks anyway
173+
# self.in_atomic_block = False
174+
# self.needs_rollback = False
175+
with self.wrap_database_errors:
176+
return self.connection.close()
171177

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

django_iris/compiler.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from django.core.exceptions import EmptyResultSet
2-
from django.db.models.expressions import Col
1+
from django.core.exceptions import EmptyResultSet, FullResultSet
2+
from django.db.models.expressions import Col, Value
33
from django.db.models.sql import compiler
44

55
class SQLCompiler(compiler.SQLCompiler):
6+
67
def as_sql(self, with_limits=True, with_col_aliases=False):
7-
with_limit_offset = with_limits and (
8-
self.query.high_mark is not None or self.query.low_mark
8+
with_limit_offset = (with_limits or self.query.is_sliced) and (
9+
self.query.high_mark is not None or self.query.low_mark > 0
910
)
1011
if self.query.select_for_update or not with_limit_offset:
1112
return super().as_sql(with_limits, with_col_aliases)
@@ -28,15 +29,14 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
2829
raise
2930
# Use a predicate that's always False.
3031
where, w_params = "0 = 1", []
32+
except FullResultSet:
33+
where, w_params = "", []
3134
having, h_params = (
3235
self.compile(self.having) if self.having is not None else ("", [])
3336
)
3437
result = ["SELECT"]
3538
params = []
3639

37-
if not offset:
38-
result.append("TOP %d" % limit)
39-
4040
if self.query.distinct:
4141
distinct_result, distinct_params = self.connection.ops.distinct_sql(
4242
distinct_fields,
@@ -45,6 +45,9 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
4545
result += distinct_result
4646
params += distinct_params
4747

48+
if not offset:
49+
result.append("TOP %d" % limit)
50+
4851
first_col = ""
4952
out_cols = []
5053
col_idx = 1
@@ -162,15 +165,36 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
162165

163166

164167
class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
165-
pass
168+
169+
def as_sql(self):
170+
if self.query.fields:
171+
return super().as_sql()
172+
173+
# No values provided
174+
175+
qn = self.connection.ops.quote_name
176+
opts = self.query.get_meta()
177+
insert_statement = self.connection.ops.insert_statement(
178+
on_conflict=self.query.on_conflict,
179+
)
180+
result = ["%s %s" % (insert_statement, qn(opts.db_table))]
181+
fields = self.query.fields or [opts.pk]
182+
result.append("(%s)" % ", ".join(qn(f.column) for f in fields))
183+
result.append("DEFAULT VALUES")
184+
185+
return [(" ".join(result), [])]
166186

167187

168188
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
169189
pass
170190

171191

172192
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
173-
pass
193+
def as_sql(self):
194+
sql, params = super().as_sql()
195+
if self.connection._disable_constraint_checking:
196+
sql = "UPDATE %%NOCHECK" + sql[6:]
197+
return sql, params
174198

175199

176200
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):

0 commit comments

Comments
 (0)