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.
@@ -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