sqlalchemy-cratedb 0.38.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_cratedb/__init__.py +62 -0
- sqlalchemy_cratedb/compat/__init__.py +0 -0
- sqlalchemy_cratedb/compat/api13.py +156 -0
- sqlalchemy_cratedb/compat/core10.py +264 -0
- sqlalchemy_cratedb/compat/core14.py +359 -0
- sqlalchemy_cratedb/compat/core20.py +447 -0
- sqlalchemy_cratedb/compiler.py +372 -0
- sqlalchemy_cratedb/dialect.py +381 -0
- sqlalchemy_cratedb/predicate.py +99 -0
- sqlalchemy_cratedb/sa_version.py +28 -0
- sqlalchemy_cratedb/support/__init__.py +14 -0
- sqlalchemy_cratedb/support/pandas.py +111 -0
- sqlalchemy_cratedb/support/polyfill.py +125 -0
- sqlalchemy_cratedb/support/util.py +41 -0
- sqlalchemy_cratedb/type/__init__.py +4 -0
- sqlalchemy_cratedb/type/array.py +144 -0
- sqlalchemy_cratedb/type/geo.py +48 -0
- sqlalchemy_cratedb/type/object.py +92 -0
- sqlalchemy_cratedb/type/vector.py +173 -0
- sqlalchemy_cratedb-0.38.0.dist-info/LICENSE +178 -0
- sqlalchemy_cratedb-0.38.0.dist-info/METADATA +143 -0
- sqlalchemy_cratedb-0.38.0.dist-info/NOTICE +24 -0
- sqlalchemy_cratedb-0.38.0.dist-info/RECORD +26 -0
- sqlalchemy_cratedb-0.38.0.dist-info/WHEEL +5 -0
- sqlalchemy_cratedb-0.38.0.dist-info/entry_points.txt +2 -0
- sqlalchemy_cratedb-0.38.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,372 @@
|
|
1
|
+
# -*- coding: utf-8; -*-
|
2
|
+
#
|
3
|
+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
|
4
|
+
# license agreements. See the NOTICE file distributed with this work for
|
5
|
+
# additional information regarding copyright ownership. Crate licenses
|
6
|
+
# this file to you under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License. You may
|
8
|
+
# obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
14
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
15
|
+
# License for the specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
#
|
18
|
+
# However, if you have executed another commercial license agreement
|
19
|
+
# with Crate these terms will supersede the license and you may use the
|
20
|
+
# software solely pursuant to the terms of the relevant commercial agreement.
|
21
|
+
|
22
|
+
import string
|
23
|
+
import warnings
|
24
|
+
from collections import defaultdict
|
25
|
+
|
26
|
+
import sqlalchemy as sa
|
27
|
+
from sqlalchemy.dialects.postgresql.base import PGCompiler
|
28
|
+
from sqlalchemy.dialects.postgresql.base import RESERVED_WORDS as POSTGRESQL_RESERVED_WORDS
|
29
|
+
from sqlalchemy.sql import compiler
|
30
|
+
from sqlalchemy.types import String
|
31
|
+
from .type.geo import Geopoint, Geoshape
|
32
|
+
from .type.object import MutableDict, ObjectTypeImpl
|
33
|
+
from .sa_version import SA_VERSION, SA_1_4
|
34
|
+
|
35
|
+
|
36
|
+
def rewrite_update(clauseelement, multiparams, params):
|
37
|
+
""" change the params to enable partial updates
|
38
|
+
|
39
|
+
sqlalchemy by default only supports updates of complex types in the form of
|
40
|
+
|
41
|
+
"col = ?", ({"x": 1, "y": 2}
|
42
|
+
|
43
|
+
but crate supports
|
44
|
+
|
45
|
+
"col['x'] = ?, col['y'] = ?", (1, 2)
|
46
|
+
|
47
|
+
by using the `ObjectType` (`MutableDict`) type.
|
48
|
+
The update statement is only rewritten if an item of the MutableDict was
|
49
|
+
changed.
|
50
|
+
"""
|
51
|
+
newmultiparams = []
|
52
|
+
_multiparams = multiparams[0]
|
53
|
+
if len(_multiparams) == 0:
|
54
|
+
return clauseelement, multiparams, params
|
55
|
+
for _params in _multiparams:
|
56
|
+
newparams = {}
|
57
|
+
for key, val in _params.items():
|
58
|
+
if (
|
59
|
+
not isinstance(val, MutableDict) or
|
60
|
+
(not any(val._changed_keys) and not any(val._deleted_keys))
|
61
|
+
):
|
62
|
+
newparams[key] = val
|
63
|
+
continue
|
64
|
+
|
65
|
+
for subkey, subval in val.items():
|
66
|
+
if subkey in val._changed_keys:
|
67
|
+
newparams["{0}['{1}']".format(key, subkey)] = subval
|
68
|
+
for subkey in val._deleted_keys:
|
69
|
+
newparams["{0}['{1}']".format(key, subkey)] = None
|
70
|
+
newmultiparams.append(newparams)
|
71
|
+
_multiparams = (newmultiparams, )
|
72
|
+
clause = clauseelement.values(newmultiparams[0])
|
73
|
+
clause._crate_specific = True
|
74
|
+
return clause, _multiparams, params
|
75
|
+
|
76
|
+
|
77
|
+
@sa.event.listens_for(sa.engine.Engine, "before_execute", retval=True)
|
78
|
+
def crate_before_execute(conn, clauseelement, multiparams, params, *args, **kwargs):
|
79
|
+
is_crate = type(conn.dialect).__name__ == 'CrateDialect'
|
80
|
+
if is_crate and isinstance(clauseelement, sa.sql.expression.Update):
|
81
|
+
if SA_VERSION >= SA_1_4:
|
82
|
+
if params is None:
|
83
|
+
multiparams = ([],)
|
84
|
+
else:
|
85
|
+
multiparams = ([params],)
|
86
|
+
params = {}
|
87
|
+
|
88
|
+
clauseelement, multiparams, params = rewrite_update(clauseelement, multiparams, params)
|
89
|
+
|
90
|
+
if SA_VERSION >= SA_1_4:
|
91
|
+
if multiparams[0]:
|
92
|
+
params = multiparams[0][0]
|
93
|
+
else:
|
94
|
+
params = multiparams[0]
|
95
|
+
multiparams = []
|
96
|
+
|
97
|
+
return clauseelement, multiparams, params
|
98
|
+
|
99
|
+
|
100
|
+
class CrateDDLCompiler(compiler.DDLCompiler):
|
101
|
+
|
102
|
+
__special_opts_tmpl = {
|
103
|
+
'partitioned_by': ' PARTITIONED BY ({0})'
|
104
|
+
}
|
105
|
+
__clustered_opts_tmpl = {
|
106
|
+
'number_of_shards': ' INTO {0} SHARDS',
|
107
|
+
'clustered_by': ' BY ({0})',
|
108
|
+
}
|
109
|
+
__clustered_opt_tmpl = ' CLUSTERED{clustered_by}{number_of_shards}'
|
110
|
+
|
111
|
+
def get_column_specification(self, column, **kwargs):
|
112
|
+
colspec = self.preparer.format_column(column) + " " + \
|
113
|
+
self.dialect.type_compiler.process(column.type)
|
114
|
+
|
115
|
+
default = self.get_column_default_string(column)
|
116
|
+
if default is not None:
|
117
|
+
colspec += " DEFAULT " + default
|
118
|
+
|
119
|
+
if column.computed is not None:
|
120
|
+
colspec += " " + self.process(column.computed)
|
121
|
+
|
122
|
+
if column.nullable is False:
|
123
|
+
colspec += " NOT NULL"
|
124
|
+
elif column.nullable and column.primary_key:
|
125
|
+
raise sa.exc.CompileError(
|
126
|
+
"Primary key columns cannot be nullable"
|
127
|
+
)
|
128
|
+
|
129
|
+
if column.dialect_options['crate'].get('index') is False:
|
130
|
+
if isinstance(column.type, (Geopoint, Geoshape, ObjectTypeImpl)):
|
131
|
+
raise sa.exc.CompileError(
|
132
|
+
"Disabling indexing is not supported for column "
|
133
|
+
"types OBJECT, GEO_POINT, and GEO_SHAPE"
|
134
|
+
)
|
135
|
+
|
136
|
+
colspec += " INDEX OFF"
|
137
|
+
|
138
|
+
if column.dialect_options['crate'].get('columnstore') is False:
|
139
|
+
if not isinstance(column.type, (String, )):
|
140
|
+
raise sa.exc.CompileError(
|
141
|
+
"Controlling the columnstore is only allowed for STRING columns"
|
142
|
+
)
|
143
|
+
|
144
|
+
colspec += " STORAGE WITH (columnstore = false)"
|
145
|
+
|
146
|
+
return colspec
|
147
|
+
|
148
|
+
def visit_computed_column(self, generated):
|
149
|
+
if generated.persisted is False:
|
150
|
+
raise sa.exc.CompileError(
|
151
|
+
"Virtual computed columns are not supported, set "
|
152
|
+
"'persisted' to None or True"
|
153
|
+
)
|
154
|
+
|
155
|
+
return "GENERATED ALWAYS AS (%s)" % self.sql_compiler.process(
|
156
|
+
generated.sqltext, include_table=False, literal_binds=True
|
157
|
+
)
|
158
|
+
|
159
|
+
def post_create_table(self, table):
|
160
|
+
special_options = ''
|
161
|
+
clustered_options = defaultdict(str)
|
162
|
+
table_opts = []
|
163
|
+
|
164
|
+
opts = dict(
|
165
|
+
(k[len(self.dialect.name) + 1:], v)
|
166
|
+
for k, v, in table.kwargs.items()
|
167
|
+
if k.startswith('%s_' % self.dialect.name)
|
168
|
+
)
|
169
|
+
for k, v in opts.items():
|
170
|
+
if k in self.__special_opts_tmpl:
|
171
|
+
special_options += self.__special_opts_tmpl[k].format(v)
|
172
|
+
elif k in self.__clustered_opts_tmpl:
|
173
|
+
clustered_options[k] = self.__clustered_opts_tmpl[k].format(v)
|
174
|
+
else:
|
175
|
+
table_opts.append('{0} = {1}'.format(k, v))
|
176
|
+
if clustered_options:
|
177
|
+
special_options += string.Formatter().vformat(
|
178
|
+
self.__clustered_opt_tmpl, (), clustered_options)
|
179
|
+
if table_opts:
|
180
|
+
return special_options + ' WITH ({0})'.format(
|
181
|
+
', '.join(sorted(table_opts)))
|
182
|
+
return special_options
|
183
|
+
|
184
|
+
def visit_foreign_key_constraint(self, constraint, **kw):
|
185
|
+
"""
|
186
|
+
CrateDB does not support foreign key constraints.
|
187
|
+
"""
|
188
|
+
warnings.warn("CrateDB does not support foreign key constraints, "
|
189
|
+
"they will be omitted when generating DDL statements.")
|
190
|
+
return None
|
191
|
+
|
192
|
+
def visit_unique_constraint(self, constraint, **kw):
|
193
|
+
"""
|
194
|
+
CrateDB does not support unique key constraints.
|
195
|
+
"""
|
196
|
+
warnings.warn("CrateDB does not support unique constraints, "
|
197
|
+
"they will be omitted when generating DDL statements.")
|
198
|
+
return None
|
199
|
+
|
200
|
+
|
201
|
+
class CrateTypeCompiler(compiler.GenericTypeCompiler):
|
202
|
+
|
203
|
+
def visit_string(self, type_, **kw):
|
204
|
+
return 'STRING'
|
205
|
+
|
206
|
+
def visit_unicode(self, type_, **kw):
|
207
|
+
return 'STRING'
|
208
|
+
|
209
|
+
def visit_TEXT(self, type_, **kw):
|
210
|
+
return 'STRING'
|
211
|
+
|
212
|
+
def visit_DECIMAL(self, type_, **kw):
|
213
|
+
return 'DOUBLE'
|
214
|
+
|
215
|
+
def visit_BIGINT(self, type_, **kw):
|
216
|
+
return 'LONG'
|
217
|
+
|
218
|
+
def visit_NUMERIC(self, type_, **kw):
|
219
|
+
return 'LONG'
|
220
|
+
|
221
|
+
def visit_INTEGER(self, type_, **kw):
|
222
|
+
return 'INT'
|
223
|
+
|
224
|
+
def visit_SMALLINT(self, type_, **kw):
|
225
|
+
return 'SHORT'
|
226
|
+
|
227
|
+
def visit_datetime(self, type_, **kw):
|
228
|
+
return self.visit_TIMESTAMP(type_, **kw)
|
229
|
+
|
230
|
+
def visit_date(self, type_, **kw):
|
231
|
+
return 'TIMESTAMP'
|
232
|
+
|
233
|
+
def visit_ARRAY(self, type_, **kw):
|
234
|
+
if type_.dimensions is not None and type_.dimensions > 1:
|
235
|
+
raise NotImplementedError(
|
236
|
+
"CrateDB doesn't support multidimensional arrays")
|
237
|
+
return 'ARRAY({0})'.format(self.process(type_.item_type))
|
238
|
+
|
239
|
+
def visit_OBJECT(self, type_, **kw):
|
240
|
+
return "OBJECT"
|
241
|
+
|
242
|
+
def visit_FLOAT_VECTOR(self, type_, **kw):
|
243
|
+
dimensions = type_.dimensions
|
244
|
+
if dimensions is None:
|
245
|
+
raise ValueError("FloatVector must be initialized with dimension size")
|
246
|
+
return f"FLOAT_VECTOR({dimensions})"
|
247
|
+
|
248
|
+
def visit_TIMESTAMP(self, type_, **kw):
|
249
|
+
"""
|
250
|
+
Support for `TIMESTAMP WITH|WITHOUT TIME ZONE`.
|
251
|
+
|
252
|
+
From `sqlalchemy.dialects.postgresql.base.PGTypeCompiler`.
|
253
|
+
"""
|
254
|
+
return "TIMESTAMP %s" % (
|
255
|
+
(type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",
|
256
|
+
)
|
257
|
+
|
258
|
+
|
259
|
+
class CrateCompiler(compiler.SQLCompiler):
|
260
|
+
|
261
|
+
def visit_getitem_binary(self, binary, operator, **kw):
|
262
|
+
return "{0}['{1}']".format(
|
263
|
+
self.process(binary.left, **kw),
|
264
|
+
binary.right.value
|
265
|
+
)
|
266
|
+
|
267
|
+
def visit_json_getitem_op_binary(
|
268
|
+
self, binary, operator, _cast_applied=False, **kw
|
269
|
+
):
|
270
|
+
return "{0}['{1}']".format(
|
271
|
+
self.process(binary.left, **kw),
|
272
|
+
binary.right.value
|
273
|
+
)
|
274
|
+
|
275
|
+
def visit_any(self, element, **kw):
|
276
|
+
return "%s%sANY (%s)" % (
|
277
|
+
self.process(element.left, **kw),
|
278
|
+
compiler.OPERATORS[element.operator],
|
279
|
+
self.process(element.right, **kw)
|
280
|
+
)
|
281
|
+
|
282
|
+
def visit_ilike_case_insensitive_operand(self, element, **kw):
|
283
|
+
"""
|
284
|
+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
|
285
|
+
"""
|
286
|
+
if self.dialect.has_ilike_operator():
|
287
|
+
return element.element._compiler_dispatch(self, **kw)
|
288
|
+
else:
|
289
|
+
return super().visit_ilike_case_insensitive_operand(element, **kw)
|
290
|
+
|
291
|
+
def visit_ilike_op_binary(self, binary, operator, **kw):
|
292
|
+
"""
|
293
|
+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
|
294
|
+
|
295
|
+
Do not implement the `ESCAPE` functionality, because it is not
|
296
|
+
supported by CrateDB.
|
297
|
+
"""
|
298
|
+
if binary.modifiers.get("escape", None) is not None:
|
299
|
+
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
|
300
|
+
if self.dialect.has_ilike_operator():
|
301
|
+
return "%s ILIKE %s" % (
|
302
|
+
self.process(binary.left, **kw),
|
303
|
+
self.process(binary.right, **kw),
|
304
|
+
)
|
305
|
+
else:
|
306
|
+
return super().visit_ilike_op_binary(binary, operator, **kw)
|
307
|
+
|
308
|
+
def visit_not_ilike_op_binary(self, binary, operator, **kw):
|
309
|
+
"""
|
310
|
+
Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
|
311
|
+
|
312
|
+
Do not implement the `ESCAPE` functionality, because it is not
|
313
|
+
supported by CrateDB.
|
314
|
+
"""
|
315
|
+
if binary.modifiers.get("escape", None) is not None:
|
316
|
+
raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
|
317
|
+
if self.dialect.has_ilike_operator():
|
318
|
+
return "%s NOT ILIKE %s" % (
|
319
|
+
self.process(binary.left, **kw),
|
320
|
+
self.process(binary.right, **kw),
|
321
|
+
)
|
322
|
+
else:
|
323
|
+
return super().visit_not_ilike_op_binary(binary, operator, **kw)
|
324
|
+
|
325
|
+
def limit_clause(self, select, **kw):
|
326
|
+
"""
|
327
|
+
Generate OFFSET / LIMIT clause, PostgreSQL-compatible.
|
328
|
+
"""
|
329
|
+
return PGCompiler.limit_clause(self, select, **kw)
|
330
|
+
|
331
|
+
def for_update_clause(self, select, **kw):
|
332
|
+
# CrateDB does not support the `INSERT ... FOR UPDATE` clause.
|
333
|
+
# See https://github.com/crate/crate-python/issues/577.
|
334
|
+
warnings.warn("CrateDB does not support the 'INSERT ... FOR UPDATE' clause, "
|
335
|
+
"it will be omitted when generating SQL statements.")
|
336
|
+
return ''
|
337
|
+
|
338
|
+
|
339
|
+
CRATEDB_RESERVED_WORDS = \
|
340
|
+
"add, alter, between, by, called, costs, delete, deny, directory, drop, escape, exists, " \
|
341
|
+
"extract, first, function, if, index, input, insert, last, match, nulls, object, " \
|
342
|
+
"persistent, recursive, reset, returns, revoke, set, stratify, transient, try_cast, " \
|
343
|
+
"unbounded, update".split(", ")
|
344
|
+
|
345
|
+
|
346
|
+
class CrateIdentifierPreparer(sa.sql.compiler.IdentifierPreparer):
|
347
|
+
"""
|
348
|
+
Define CrateDB's reserved words to be quoted properly.
|
349
|
+
"""
|
350
|
+
reserved_words = set(list(POSTGRESQL_RESERVED_WORDS) + CRATEDB_RESERVED_WORDS)
|
351
|
+
|
352
|
+
def _unquote_identifier(self, value):
|
353
|
+
if value[0] == self.initial_quote:
|
354
|
+
value = value[1:-1].replace(
|
355
|
+
self.escape_to_quote, self.escape_quote
|
356
|
+
)
|
357
|
+
return value
|
358
|
+
|
359
|
+
def format_type(self, type_, use_schema=True):
|
360
|
+
if not type_.name:
|
361
|
+
raise sa.exc.CompileError("Type requires a name.")
|
362
|
+
|
363
|
+
name = self.quote(type_.name)
|
364
|
+
effective_schema = self.schema_for_object(type_)
|
365
|
+
|
366
|
+
if (
|
367
|
+
not self.omit_schema
|
368
|
+
and use_schema
|
369
|
+
and effective_schema is not None
|
370
|
+
):
|
371
|
+
name = self.quote_schema(effective_schema) + "." + name
|
372
|
+
return name
|