sqlalchemy-cratedb 0.41.0.dev0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,361 @@
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 RESERVED_WORDS as POSTGRESQL_RESERVED_WORDS
28
+ from sqlalchemy.dialects.postgresql.base import PGCompiler
29
+ from sqlalchemy.sql import compiler
30
+ from sqlalchemy.types import String
31
+
32
+ from .sa_version import SA_1_4, SA_VERSION
33
+ from .type.geo import Geopoint, Geoshape
34
+ from .type.object import MutableDict, ObjectTypeImpl
35
+
36
+
37
+ def rewrite_update(clauseelement, multiparams, params):
38
+ """change the params to enable partial updates
39
+
40
+ sqlalchemy by default only supports updates of complex types in the form of
41
+
42
+ "col = ?", ({"x": 1, "y": 2}
43
+
44
+ but crate supports
45
+
46
+ "col['x'] = ?, col['y'] = ?", (1, 2)
47
+
48
+ by using the `ObjectType` (`MutableDict`) type.
49
+ The update statement is only rewritten if an item of the MutableDict was
50
+ changed.
51
+ """
52
+ newmultiparams = []
53
+ _multiparams = multiparams[0]
54
+ if len(_multiparams) == 0:
55
+ return clauseelement, multiparams, params
56
+ for _params in _multiparams:
57
+ newparams = {}
58
+ for key, val in _params.items():
59
+ if 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
+ __special_opts_tmpl = {"partitioned_by": " PARTITIONED BY ({0})"}
102
+ __clustered_opts_tmpl = {
103
+ "number_of_shards": " INTO {0} SHARDS",
104
+ "clustered_by": " BY ({0})",
105
+ }
106
+ __clustered_opt_tmpl = " CLUSTERED{clustered_by}{number_of_shards}"
107
+
108
+ def get_column_specification(self, column, **kwargs):
109
+ colspec = (
110
+ self.preparer.format_column(column)
111
+ + " "
112
+ + self.dialect.type_compiler.process(column.type)
113
+ )
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("Primary key columns cannot be nullable")
126
+
127
+ if column.dialect_options["crate"].get("index") is False:
128
+ if isinstance(column.type, (Geopoint, Geoshape, ObjectTypeImpl)):
129
+ raise sa.exc.CompileError(
130
+ "Disabling indexing is not supported for column "
131
+ "types OBJECT, GEO_POINT, and GEO_SHAPE"
132
+ )
133
+
134
+ colspec += " INDEX OFF"
135
+
136
+ if column.dialect_options["crate"].get("columnstore") is False:
137
+ if not isinstance(column.type, (String,)):
138
+ raise sa.exc.CompileError(
139
+ "Controlling the columnstore is only allowed for STRING columns"
140
+ )
141
+
142
+ colspec += " STORAGE WITH (columnstore = false)"
143
+
144
+ return colspec
145
+
146
+ def visit_computed_column(self, generated):
147
+ if generated.persisted is False:
148
+ raise sa.exc.CompileError(
149
+ "Virtual computed columns are not supported, set 'persisted' to None or True"
150
+ )
151
+
152
+ return "GENERATED ALWAYS AS (%s)" % self.sql_compiler.process(
153
+ generated.sqltext, include_table=False, literal_binds=True
154
+ )
155
+
156
+ def post_create_table(self, table):
157
+ special_options = ""
158
+ clustered_options = defaultdict(str)
159
+ table_opts = []
160
+
161
+ opts = dict(
162
+ (k[len(self.dialect.name) + 1 :], v)
163
+ for k, v in table.kwargs.items()
164
+ if k.startswith("%s_" % self.dialect.name)
165
+ )
166
+ for k, v in opts.items():
167
+ if k in self.__special_opts_tmpl:
168
+ special_options += self.__special_opts_tmpl[k].format(v)
169
+ elif k in self.__clustered_opts_tmpl:
170
+ clustered_options[k] = self.__clustered_opts_tmpl[k].format(v)
171
+ else:
172
+ table_opts.append("{0} = {1}".format(k, v))
173
+ if clustered_options:
174
+ special_options += string.Formatter().vformat(
175
+ self.__clustered_opt_tmpl, (), clustered_options
176
+ )
177
+ if table_opts:
178
+ return special_options + " WITH ({0})".format(", ".join(sorted(table_opts)))
179
+ return special_options
180
+
181
+ def visit_foreign_key_constraint(self, constraint, **kw):
182
+ """
183
+ CrateDB does not support foreign key constraints.
184
+ """
185
+ warnings.warn(
186
+ "CrateDB does not support foreign key constraints, "
187
+ "they will be omitted when generating DDL statements.",
188
+ stacklevel=2,
189
+ )
190
+ return
191
+
192
+ def visit_unique_constraint(self, constraint, **kw):
193
+ """
194
+ CrateDB does not support unique key constraints.
195
+ """
196
+ warnings.warn(
197
+ "CrateDB does not support unique constraints, "
198
+ "they will be omitted when generating DDL statements.",
199
+ stacklevel=2,
200
+ )
201
+ return
202
+
203
+
204
+ class CrateTypeCompiler(compiler.GenericTypeCompiler):
205
+ def visit_string(self, type_, **kw):
206
+ return "STRING"
207
+
208
+ def visit_unicode(self, type_, **kw):
209
+ return "STRING"
210
+
211
+ def visit_TEXT(self, type_, **kw):
212
+ return "STRING"
213
+
214
+ def visit_DECIMAL(self, type_, **kw):
215
+ return "DOUBLE"
216
+
217
+ def visit_BIGINT(self, type_, **kw):
218
+ return "LONG"
219
+
220
+ def visit_NUMERIC(self, type_, **kw):
221
+ return "LONG"
222
+
223
+ def visit_INTEGER(self, type_, **kw):
224
+ return "INT"
225
+
226
+ def visit_SMALLINT(self, type_, **kw):
227
+ return "SHORT"
228
+
229
+ def visit_datetime(self, type_, **kw):
230
+ return self.visit_TIMESTAMP(type_, **kw)
231
+
232
+ def visit_date(self, type_, **kw):
233
+ return "TIMESTAMP"
234
+
235
+ def visit_ARRAY(self, type_, **kw):
236
+ if type_.dimensions is not None and type_.dimensions > 1:
237
+ raise NotImplementedError("CrateDB doesn't support multidimensional arrays")
238
+ return "ARRAY({0})".format(self.process(type_.item_type))
239
+
240
+ def visit_OBJECT(self, type_, **kw):
241
+ return "OBJECT"
242
+
243
+ def visit_FLOAT_VECTOR(self, type_, **kw):
244
+ dimensions = type_.dimensions
245
+ if dimensions is None:
246
+ raise ValueError("FloatVector must be initialized with dimension size")
247
+ return f"FLOAT_VECTOR({dimensions})"
248
+
249
+ def visit_TIMESTAMP(self, type_, **kw):
250
+ """
251
+ Support for `TIMESTAMP WITH|WITHOUT TIME ZONE`.
252
+
253
+ From `sqlalchemy.dialects.postgresql.base.PGTypeCompiler`.
254
+ """
255
+ return "TIMESTAMP %s" % ((type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",)
256
+
257
+
258
+ class CrateCompiler(compiler.SQLCompiler):
259
+ def visit_getitem_binary(self, binary, operator, **kw):
260
+ return "{0}['{1}']".format(self.process(binary.left, **kw), binary.right.value)
261
+
262
+ def visit_json_getitem_op_binary(self, binary, operator, _cast_applied=False, **kw):
263
+ return "{0}['{1}']".format(self.process(binary.left, **kw), binary.right.value)
264
+
265
+ def visit_any(self, element, **kw):
266
+ return "%s%sANY (%s)" % (
267
+ self.process(element.left, **kw),
268
+ compiler.OPERATORS[element.operator],
269
+ self.process(element.right, **kw),
270
+ )
271
+
272
+ def visit_ilike_case_insensitive_operand(self, element, **kw):
273
+ """
274
+ Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
275
+ """
276
+ if self.dialect.has_ilike_operator():
277
+ return element.element._compiler_dispatch(self, **kw)
278
+ else:
279
+ return super().visit_ilike_case_insensitive_operand(element, **kw)
280
+
281
+ def visit_ilike_op_binary(self, binary, operator, **kw):
282
+ """
283
+ Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
284
+
285
+ Do not implement the `ESCAPE` functionality, because it is not
286
+ supported by CrateDB.
287
+ """
288
+ if binary.modifiers.get("escape", None) is not None:
289
+ raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
290
+ if self.dialect.has_ilike_operator():
291
+ return "%s ILIKE %s" % (
292
+ self.process(binary.left, **kw),
293
+ self.process(binary.right, **kw),
294
+ )
295
+ else:
296
+ return super().visit_ilike_op_binary(binary, operator, **kw)
297
+
298
+ def visit_not_ilike_op_binary(self, binary, operator, **kw):
299
+ """
300
+ Use native `ILIKE` operator, like PostgreSQL's `PGCompiler`.
301
+
302
+ Do not implement the `ESCAPE` functionality, because it is not
303
+ supported by CrateDB.
304
+ """
305
+ if binary.modifiers.get("escape", None) is not None:
306
+ raise NotImplementedError("Unsupported feature: ESCAPE is not supported")
307
+ if self.dialect.has_ilike_operator():
308
+ return "%s NOT ILIKE %s" % (
309
+ self.process(binary.left, **kw),
310
+ self.process(binary.right, **kw),
311
+ )
312
+ else:
313
+ return super().visit_not_ilike_op_binary(binary, operator, **kw)
314
+
315
+ def limit_clause(self, select, **kw):
316
+ """
317
+ Generate OFFSET / LIMIT clause, PostgreSQL-compatible.
318
+ """
319
+ return PGCompiler.limit_clause(self, select, **kw)
320
+
321
+ def for_update_clause(self, select, **kw):
322
+ # CrateDB does not support the `INSERT ... FOR UPDATE` clause.
323
+ # See https://github.com/crate/crate-python/issues/577.
324
+ warnings.warn(
325
+ "CrateDB does not support the 'INSERT ... FOR UPDATE' clause, "
326
+ "it will be omitted when generating SQL statements.",
327
+ stacklevel=2,
328
+ )
329
+ return ""
330
+
331
+
332
+ CRATEDB_RESERVED_WORDS = (
333
+ "add, alter, between, by, called, costs, delete, deny, directory, drop, escape, exists, "
334
+ "extract, first, function, if, index, input, insert, last, match, nulls, object, "
335
+ "persistent, recursive, reset, returns, revoke, set, stratify, transient, try_cast, "
336
+ "unbounded, update".split(", ")
337
+ )
338
+
339
+
340
+ class CrateIdentifierPreparer(sa.sql.compiler.IdentifierPreparer):
341
+ """
342
+ Define CrateDB's reserved words to be quoted properly.
343
+ """
344
+
345
+ reserved_words = set(list(POSTGRESQL_RESERVED_WORDS) + CRATEDB_RESERVED_WORDS)
346
+
347
+ def _unquote_identifier(self, value):
348
+ if value[0] == self.initial_quote:
349
+ value = value[1:-1].replace(self.escape_to_quote, self.escape_quote)
350
+ return value
351
+
352
+ def format_type(self, type_, use_schema=True):
353
+ if not type_.name:
354
+ raise sa.exc.CompileError("Type requires a name.")
355
+
356
+ name = self.quote(type_.name)
357
+ effective_schema = self.schema_for_object(type_)
358
+
359
+ if not self.omit_schema and use_schema and effective_schema is not None:
360
+ name = self.quote_schema(effective_schema) + "." + name
361
+ return name