snowflake-sqlalchemy 1.7.3__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.
- snowflake/sqlalchemy/__init__.py +162 -0
- snowflake/sqlalchemy/_constants.py +14 -0
- snowflake/sqlalchemy/base.py +1188 -0
- snowflake/sqlalchemy/compat.py +36 -0
- snowflake/sqlalchemy/custom_commands.py +627 -0
- snowflake/sqlalchemy/custom_types.py +155 -0
- snowflake/sqlalchemy/exc.py +82 -0
- snowflake/sqlalchemy/functions.py +16 -0
- snowflake/sqlalchemy/parser/custom_type_parser.py +245 -0
- snowflake/sqlalchemy/provision.py +12 -0
- snowflake/sqlalchemy/requirements.py +313 -0
- snowflake/sqlalchemy/snowdialect.py +1029 -0
- snowflake/sqlalchemy/sql/__init__.py +3 -0
- snowflake/sqlalchemy/sql/custom_schema/__init__.py +9 -0
- snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +37 -0
- snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +127 -0
- snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +13 -0
- snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +117 -0
- snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +63 -0
- snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +102 -0
- snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +33 -0
- snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +63 -0
- snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +58 -0
- snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +63 -0
- snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +25 -0
- snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +65 -0
- snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +14 -0
- snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +67 -0
- snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +84 -0
- snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +94 -0
- snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +70 -0
- snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +54 -0
- snowflake/sqlalchemy/util.py +344 -0
- snowflake/sqlalchemy/version.py +6 -0
- snowflake_sqlalchemy-1.7.3.dist-info/METADATA +737 -0
- snowflake_sqlalchemy-1.7.3.dist-info/RECORD +39 -0
- snowflake_sqlalchemy-1.7.3.dist-info/WHEEL +4 -0
- snowflake_sqlalchemy-1.7.3.dist-info/entry_points.txt +2 -0
- snowflake_sqlalchemy-1.7.3.dist-info/licenses/LICENSE.txt +202 -0
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import operator
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
import warnings
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from sqlalchemy import exc as sa_exc
|
|
13
|
+
from sqlalchemy import inspect, sql
|
|
14
|
+
from sqlalchemy import util as sa_util
|
|
15
|
+
from sqlalchemy.engine import default
|
|
16
|
+
from sqlalchemy.orm import context
|
|
17
|
+
from sqlalchemy.orm.context import _MapperEntity
|
|
18
|
+
from sqlalchemy.schema import Sequence, Table
|
|
19
|
+
from sqlalchemy.sql import compiler, expression, functions
|
|
20
|
+
from sqlalchemy.sql.base import CompileState
|
|
21
|
+
from sqlalchemy.sql.elements import BindParameter, quoted_name
|
|
22
|
+
from sqlalchemy.sql.expression import Executable
|
|
23
|
+
from sqlalchemy.sql.selectable import Lateral, SelectState
|
|
24
|
+
|
|
25
|
+
from snowflake.sqlalchemy._constants import DIALECT_NAME
|
|
26
|
+
from snowflake.sqlalchemy.compat import IS_VERSION_20, args_reducer, string_types
|
|
27
|
+
from snowflake.sqlalchemy.custom_commands import (
|
|
28
|
+
AWSBucket,
|
|
29
|
+
AzureContainer,
|
|
30
|
+
ExternalStage,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from ._constants import NOT_NULL
|
|
34
|
+
from .exc import (
|
|
35
|
+
CustomOptionsAreOnlySupportedOnSnowflakeTables,
|
|
36
|
+
UnexpectedOptionTypeError,
|
|
37
|
+
)
|
|
38
|
+
from .functions import flatten
|
|
39
|
+
from .sql.custom_schema.custom_table_base import CustomTableBase
|
|
40
|
+
from .sql.custom_schema.options.table_option import TableOption
|
|
41
|
+
from .util import (
|
|
42
|
+
_find_left_clause_to_join_from,
|
|
43
|
+
_set_connection_interpolate_empty_sequences,
|
|
44
|
+
_Snowflake_ORMJoin,
|
|
45
|
+
_Snowflake_Selectable_Join,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
RESERVED_WORDS = frozenset(
|
|
49
|
+
[
|
|
50
|
+
"ALL", # ANSI Reserved words
|
|
51
|
+
"ALTER",
|
|
52
|
+
"AND",
|
|
53
|
+
"ANY",
|
|
54
|
+
"AS",
|
|
55
|
+
"BETWEEN",
|
|
56
|
+
"BY",
|
|
57
|
+
"CHECK",
|
|
58
|
+
"COLUMN",
|
|
59
|
+
"CONNECT",
|
|
60
|
+
"COPY",
|
|
61
|
+
"CREATE",
|
|
62
|
+
"CURRENT",
|
|
63
|
+
"DELETE",
|
|
64
|
+
"DISTINCT",
|
|
65
|
+
"DROP",
|
|
66
|
+
"ELSE",
|
|
67
|
+
"EXISTS",
|
|
68
|
+
"FOR",
|
|
69
|
+
"FROM",
|
|
70
|
+
"GRANT",
|
|
71
|
+
"GROUP",
|
|
72
|
+
"HAVING",
|
|
73
|
+
"IN",
|
|
74
|
+
"INSERT",
|
|
75
|
+
"INTERSECT",
|
|
76
|
+
"INTO",
|
|
77
|
+
"IS",
|
|
78
|
+
"LIKE",
|
|
79
|
+
"NOT",
|
|
80
|
+
"NULL",
|
|
81
|
+
"OF",
|
|
82
|
+
"ON",
|
|
83
|
+
"OR",
|
|
84
|
+
"ORDER",
|
|
85
|
+
"REVOKE",
|
|
86
|
+
"ROW",
|
|
87
|
+
"ROWS",
|
|
88
|
+
"SAMPLE",
|
|
89
|
+
"SELECT",
|
|
90
|
+
"SET",
|
|
91
|
+
"START",
|
|
92
|
+
"TABLE",
|
|
93
|
+
"THEN",
|
|
94
|
+
"TO",
|
|
95
|
+
"TRIGGER",
|
|
96
|
+
"UNION",
|
|
97
|
+
"UNIQUE",
|
|
98
|
+
"UPDATE",
|
|
99
|
+
"VALUES",
|
|
100
|
+
"WHENEVER",
|
|
101
|
+
"WHERE",
|
|
102
|
+
"WITH",
|
|
103
|
+
"REGEXP",
|
|
104
|
+
"RLIKE",
|
|
105
|
+
"SOME", # Snowflake Reserved words
|
|
106
|
+
"MINUS",
|
|
107
|
+
"INCREMENT", # Oracle reserved words
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Snowflake DML:
|
|
112
|
+
# - UPDATE
|
|
113
|
+
# - INSERT
|
|
114
|
+
# - DELETE
|
|
115
|
+
# - MERGE
|
|
116
|
+
AUTOCOMMIT_REGEXP = re.compile(
|
|
117
|
+
r"\s*(?:UPDATE|INSERT|DELETE|MERGE|COPY)", re.I | re.UNICODE
|
|
118
|
+
)
|
|
119
|
+
# used for quoting identifiers ie. table names, column names, etc.
|
|
120
|
+
ILLEGAL_INITIAL_CHARACTERS = frozenset({d for d in string.digits}.union({"$"}))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# used for quoting identifiers ie. table names, column names, etc.
|
|
124
|
+
ILLEGAL_IDENTIFIERS = frozenset({d for d in string.digits}.union({"_"}))
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
Overwrite methods to handle Snowflake BCR change:
|
|
128
|
+
https://docs.snowflake.com/en/release-notes/bcr-bundles/2023_04/bcr-1057
|
|
129
|
+
- _join_determine_implicit_left_side
|
|
130
|
+
- _join_left_to_right
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# handle Snowflake BCR bcr-1057
|
|
135
|
+
@CompileState.plugin_for("default", "select")
|
|
136
|
+
class SnowflakeSelectState(SelectState):
|
|
137
|
+
def _setup_joins(self, args, raw_columns):
|
|
138
|
+
for right, onclause, left, flags in args:
|
|
139
|
+
isouter = flags["isouter"]
|
|
140
|
+
full = flags["full"]
|
|
141
|
+
|
|
142
|
+
if left is None:
|
|
143
|
+
(
|
|
144
|
+
left,
|
|
145
|
+
replace_from_obj_index,
|
|
146
|
+
) = self._join_determine_implicit_left_side(
|
|
147
|
+
raw_columns, left, right, onclause
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
(replace_from_obj_index) = self._join_place_explicit_left_side(left)
|
|
151
|
+
|
|
152
|
+
if replace_from_obj_index is not None:
|
|
153
|
+
# splice into an existing element in the
|
|
154
|
+
# self._from_obj list
|
|
155
|
+
left_clause = self.from_clauses[replace_from_obj_index]
|
|
156
|
+
|
|
157
|
+
self.from_clauses = (
|
|
158
|
+
self.from_clauses[:replace_from_obj_index]
|
|
159
|
+
+ (
|
|
160
|
+
_Snowflake_Selectable_Join( # handle Snowflake BCR bcr-1057
|
|
161
|
+
left_clause,
|
|
162
|
+
right,
|
|
163
|
+
onclause,
|
|
164
|
+
isouter=isouter,
|
|
165
|
+
full=full,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
+ self.from_clauses[replace_from_obj_index + 1 :]
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
self.from_clauses = self.from_clauses + (
|
|
172
|
+
# handle Snowflake BCR bcr-1057
|
|
173
|
+
_Snowflake_Selectable_Join(
|
|
174
|
+
left, right, onclause, isouter=isouter, full=full
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@sa_util.preload_module("sqlalchemy.sql.util")
|
|
179
|
+
def _join_determine_implicit_left_side(self, raw_columns, left, right, onclause):
|
|
180
|
+
"""When join conditions don't express the left side explicitly,
|
|
181
|
+
determine if an existing FROM or entity in this query
|
|
182
|
+
can serve as the left hand side.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
replace_from_obj_index = None
|
|
187
|
+
|
|
188
|
+
from_clauses = self.from_clauses
|
|
189
|
+
|
|
190
|
+
if from_clauses:
|
|
191
|
+
# handle Snowflake BCR bcr-1057
|
|
192
|
+
indexes = _find_left_clause_to_join_from(from_clauses, right, onclause)
|
|
193
|
+
|
|
194
|
+
if len(indexes) == 1:
|
|
195
|
+
replace_from_obj_index = indexes[0]
|
|
196
|
+
left = from_clauses[replace_from_obj_index]
|
|
197
|
+
else:
|
|
198
|
+
potential = {}
|
|
199
|
+
statement = self.statement
|
|
200
|
+
|
|
201
|
+
for from_clause in itertools.chain(
|
|
202
|
+
itertools.chain.from_iterable(
|
|
203
|
+
[element._from_objects for element in raw_columns]
|
|
204
|
+
),
|
|
205
|
+
itertools.chain.from_iterable(
|
|
206
|
+
[element._from_objects for element in statement._where_criteria]
|
|
207
|
+
),
|
|
208
|
+
):
|
|
209
|
+
potential[from_clause] = ()
|
|
210
|
+
|
|
211
|
+
all_clauses = list(potential.keys())
|
|
212
|
+
# handle Snowflake BCR bcr-1057
|
|
213
|
+
indexes = _find_left_clause_to_join_from(all_clauses, right, onclause)
|
|
214
|
+
|
|
215
|
+
if len(indexes) == 1:
|
|
216
|
+
left = all_clauses[indexes[0]]
|
|
217
|
+
|
|
218
|
+
if len(indexes) > 1:
|
|
219
|
+
raise sa_exc.InvalidRequestError(
|
|
220
|
+
"Can't determine which FROM clause to join "
|
|
221
|
+
"from, there are multiple FROMS which can "
|
|
222
|
+
"join to this entity. Please use the .select_from() "
|
|
223
|
+
"method to establish an explicit left side, as well as "
|
|
224
|
+
"providing an explicit ON clause if not present already to "
|
|
225
|
+
"help resolve the ambiguity."
|
|
226
|
+
)
|
|
227
|
+
elif not indexes:
|
|
228
|
+
raise sa_exc.InvalidRequestError(
|
|
229
|
+
"Don't know how to join to %r. "
|
|
230
|
+
"Please use the .select_from() "
|
|
231
|
+
"method to establish an explicit left side, as well as "
|
|
232
|
+
"providing an explicit ON clause if not present already to "
|
|
233
|
+
"help resolve the ambiguity." % (right,)
|
|
234
|
+
)
|
|
235
|
+
return left, replace_from_obj_index
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# handle Snowflake BCR bcr-1057
|
|
239
|
+
@sql.base.CompileState.plugin_for("orm", "select")
|
|
240
|
+
class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
|
|
241
|
+
def _join_determine_implicit_left_side(
|
|
242
|
+
self, entities_collection, left, right, onclause
|
|
243
|
+
):
|
|
244
|
+
"""When join conditions don't express the left side explicitly,
|
|
245
|
+
determine if an existing FROM or entity in this query
|
|
246
|
+
can serve as the left hand side.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
# when we are here, it means join() was called without an ORM-
|
|
251
|
+
# specific way of telling us what the "left" side is, e.g.:
|
|
252
|
+
#
|
|
253
|
+
# join(RightEntity)
|
|
254
|
+
#
|
|
255
|
+
# or
|
|
256
|
+
#
|
|
257
|
+
# join(RightEntity, RightEntity.foo == LeftEntity.bar)
|
|
258
|
+
#
|
|
259
|
+
|
|
260
|
+
r_info = inspect(right)
|
|
261
|
+
|
|
262
|
+
replace_from_obj_index = use_entity_index = None
|
|
263
|
+
|
|
264
|
+
if self.from_clauses:
|
|
265
|
+
# we have a list of FROMs already. So by definition this
|
|
266
|
+
# join has to connect to one of those FROMs.
|
|
267
|
+
|
|
268
|
+
# handle Snowflake BCR bcr-1057
|
|
269
|
+
indexes = _find_left_clause_to_join_from(
|
|
270
|
+
self.from_clauses, r_info.selectable, onclause
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if len(indexes) == 1:
|
|
274
|
+
replace_from_obj_index = indexes[0]
|
|
275
|
+
left = self.from_clauses[replace_from_obj_index]
|
|
276
|
+
elif len(indexes) > 1:
|
|
277
|
+
raise sa_exc.InvalidRequestError(
|
|
278
|
+
"Can't determine which FROM clause to join "
|
|
279
|
+
"from, there are multiple FROMS which can "
|
|
280
|
+
"join to this entity. Please use the .select_from() "
|
|
281
|
+
"method to establish an explicit left side, as well as "
|
|
282
|
+
"providing an explicit ON clause if not present already "
|
|
283
|
+
"to help resolve the ambiguity."
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
raise sa_exc.InvalidRequestError(
|
|
287
|
+
"Don't know how to join to %r. "
|
|
288
|
+
"Please use the .select_from() "
|
|
289
|
+
"method to establish an explicit left side, as well as "
|
|
290
|
+
"providing an explicit ON clause if not present already "
|
|
291
|
+
"to help resolve the ambiguity." % (right,)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
elif entities_collection:
|
|
295
|
+
# we have no explicit FROMs, so the implicit left has to
|
|
296
|
+
# come from our list of entities.
|
|
297
|
+
|
|
298
|
+
potential = {}
|
|
299
|
+
for entity_index, ent in enumerate(entities_collection):
|
|
300
|
+
entity = ent.entity_zero_or_selectable
|
|
301
|
+
if entity is None:
|
|
302
|
+
continue
|
|
303
|
+
ent_info = inspect(entity)
|
|
304
|
+
if ent_info is r_info: # left and right are the same, skip
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# by using a dictionary with the selectables as keys this
|
|
308
|
+
# de-duplicates those selectables as occurs when the query is
|
|
309
|
+
# against a series of columns from the same selectable
|
|
310
|
+
if isinstance(ent, context._MapperEntity):
|
|
311
|
+
potential[ent.selectable] = (entity_index, entity)
|
|
312
|
+
else:
|
|
313
|
+
potential[ent_info.selectable] = (None, entity)
|
|
314
|
+
|
|
315
|
+
all_clauses = list(potential.keys())
|
|
316
|
+
# handle Snowflake BCR bcr-1057
|
|
317
|
+
indexes = _find_left_clause_to_join_from(
|
|
318
|
+
all_clauses, r_info.selectable, onclause
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if len(indexes) == 1:
|
|
322
|
+
use_entity_index, left = potential[all_clauses[indexes[0]]]
|
|
323
|
+
elif len(indexes) > 1:
|
|
324
|
+
raise sa_exc.InvalidRequestError(
|
|
325
|
+
"Can't determine which FROM clause to join "
|
|
326
|
+
"from, there are multiple FROMS which can "
|
|
327
|
+
"join to this entity. Please use the .select_from() "
|
|
328
|
+
"method to establish an explicit left side, as well as "
|
|
329
|
+
"providing an explicit ON clause if not present already "
|
|
330
|
+
"to help resolve the ambiguity."
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
raise sa_exc.InvalidRequestError(
|
|
334
|
+
"Don't know how to join to %r. "
|
|
335
|
+
"Please use the .select_from() "
|
|
336
|
+
"method to establish an explicit left side, as well as "
|
|
337
|
+
"providing an explicit ON clause if not present already "
|
|
338
|
+
"to help resolve the ambiguity." % (right,)
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
raise sa_exc.InvalidRequestError(
|
|
342
|
+
"No entities to join from; please use "
|
|
343
|
+
"select_from() to establish the left "
|
|
344
|
+
"entity/selectable of this join"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return left, replace_from_obj_index, use_entity_index
|
|
348
|
+
|
|
349
|
+
@args_reducer(positions_to_drop=(6, 7))
|
|
350
|
+
def _join_left_to_right(
|
|
351
|
+
self, entities_collection, left, right, onclause, prop, outerjoin, full
|
|
352
|
+
):
|
|
353
|
+
"""given raw "left", "right", "onclause" parameters consumed from
|
|
354
|
+
a particular key within _join(), add a real ORMJoin object to
|
|
355
|
+
our _from_obj list (or augment an existing one)
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
if left is None:
|
|
360
|
+
# left not given (e.g. no relationship object/name specified)
|
|
361
|
+
# figure out the best "left" side based on our existing froms /
|
|
362
|
+
# entities
|
|
363
|
+
assert prop is None
|
|
364
|
+
(
|
|
365
|
+
left,
|
|
366
|
+
replace_from_obj_index,
|
|
367
|
+
use_entity_index,
|
|
368
|
+
) = self._join_determine_implicit_left_side(
|
|
369
|
+
entities_collection, left, right, onclause
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
# left is given via a relationship/name, or as explicit left side.
|
|
373
|
+
# Determine where in our
|
|
374
|
+
# "froms" list it should be spliced/appended as well as what
|
|
375
|
+
# existing entity it corresponds to.
|
|
376
|
+
(
|
|
377
|
+
replace_from_obj_index,
|
|
378
|
+
use_entity_index,
|
|
379
|
+
) = self._join_place_explicit_left_side(entities_collection, left)
|
|
380
|
+
|
|
381
|
+
if left is right:
|
|
382
|
+
raise sa_exc.InvalidRequestError(
|
|
383
|
+
"Can't construct a join from %s to %s, they "
|
|
384
|
+
"are the same entity" % (left, right)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# the right side as given often needs to be adapted. additionally
|
|
388
|
+
# a lot of things can be wrong with it. handle all that and
|
|
389
|
+
# get back the new effective "right" side
|
|
390
|
+
|
|
391
|
+
if IS_VERSION_20:
|
|
392
|
+
r_info, right, onclause = self._join_check_and_adapt_right_side(
|
|
393
|
+
left, right, onclause, prop
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
r_info, right, onclause = self._join_check_and_adapt_right_side(
|
|
397
|
+
left, right, onclause, prop, False, False
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if not r_info.is_selectable:
|
|
401
|
+
extra_criteria = self._get_extra_criteria(r_info)
|
|
402
|
+
else:
|
|
403
|
+
extra_criteria = ()
|
|
404
|
+
|
|
405
|
+
if replace_from_obj_index is not None:
|
|
406
|
+
# splice into an existing element in the
|
|
407
|
+
# self._from_obj list
|
|
408
|
+
left_clause = self.from_clauses[replace_from_obj_index]
|
|
409
|
+
|
|
410
|
+
self.from_clauses = (
|
|
411
|
+
self.from_clauses[:replace_from_obj_index]
|
|
412
|
+
+ [
|
|
413
|
+
_Snowflake_ORMJoin( # handle Snowflake BCR bcr-1057
|
|
414
|
+
left_clause,
|
|
415
|
+
right,
|
|
416
|
+
onclause,
|
|
417
|
+
isouter=outerjoin,
|
|
418
|
+
full=full,
|
|
419
|
+
_extra_criteria=extra_criteria,
|
|
420
|
+
)
|
|
421
|
+
]
|
|
422
|
+
+ self.from_clauses[replace_from_obj_index + 1 :]
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
# add a new element to the self._from_obj list
|
|
426
|
+
if use_entity_index is not None:
|
|
427
|
+
# make use of _MapperEntity selectable, which is usually
|
|
428
|
+
# entity_zero.selectable, but if with_polymorphic() were used
|
|
429
|
+
# might be distinct
|
|
430
|
+
assert isinstance(entities_collection[use_entity_index], _MapperEntity)
|
|
431
|
+
left_clause = entities_collection[use_entity_index].selectable
|
|
432
|
+
else:
|
|
433
|
+
left_clause = left
|
|
434
|
+
|
|
435
|
+
self.from_clauses = self.from_clauses + [
|
|
436
|
+
_Snowflake_ORMJoin( # handle Snowflake BCR bcr-1057
|
|
437
|
+
left_clause,
|
|
438
|
+
r_info,
|
|
439
|
+
onclause,
|
|
440
|
+
isouter=outerjoin,
|
|
441
|
+
full=full,
|
|
442
|
+
_extra_criteria=extra_criteria,
|
|
443
|
+
)
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class SnowflakeIdentifierPreparer(compiler.IdentifierPreparer):
|
|
448
|
+
reserved_words = {x.lower() for x in RESERVED_WORDS}
|
|
449
|
+
illegal_initial_characters = ILLEGAL_INITIAL_CHARACTERS
|
|
450
|
+
illegal_identifiers = ILLEGAL_IDENTIFIERS
|
|
451
|
+
|
|
452
|
+
def __init__(self, dialect, **kw):
|
|
453
|
+
quote = '"'
|
|
454
|
+
|
|
455
|
+
super().__init__(dialect, initial_quote=quote, escape_quote=quote)
|
|
456
|
+
|
|
457
|
+
def _quote_free_identifiers(self, *ids):
|
|
458
|
+
"""
|
|
459
|
+
Unilaterally identifier-quote any number of strings.
|
|
460
|
+
"""
|
|
461
|
+
return tuple(self.quote(i) for i in ids if i is not None)
|
|
462
|
+
|
|
463
|
+
def quote_schema(self, schema, force=None):
|
|
464
|
+
"""
|
|
465
|
+
Split schema by a dot and merge with required quotes
|
|
466
|
+
"""
|
|
467
|
+
idents = self._split_schema_by_dot(schema)
|
|
468
|
+
return ".".join(self._quote_free_identifiers(*idents))
|
|
469
|
+
|
|
470
|
+
def format_label(self, label, name=None):
|
|
471
|
+
n = name or label.name
|
|
472
|
+
s = n.replace(self.escape_quote, "")
|
|
473
|
+
|
|
474
|
+
if not isinstance(n, quoted_name) or n.quote is None:
|
|
475
|
+
return self.quote(s)
|
|
476
|
+
|
|
477
|
+
return self.quote_identifier(s) if n.quote else s
|
|
478
|
+
|
|
479
|
+
def _requires_quotes(self, value: str) -> bool:
|
|
480
|
+
"""Return True if the given identifier requires quoting."""
|
|
481
|
+
lc_value = value.lower()
|
|
482
|
+
return (
|
|
483
|
+
lc_value in self.reserved_words
|
|
484
|
+
or lc_value in self.illegal_identifiers
|
|
485
|
+
or value[0] in self.illegal_initial_characters
|
|
486
|
+
or not self.legal_characters.match(str(value))
|
|
487
|
+
or (lc_value != value)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def _split_schema_by_dot(self, schema):
|
|
491
|
+
ret = []
|
|
492
|
+
idx = 0
|
|
493
|
+
pre_idx = 0
|
|
494
|
+
in_quote = False
|
|
495
|
+
while idx < len(schema):
|
|
496
|
+
if not in_quote:
|
|
497
|
+
if schema[idx] == "." and pre_idx < idx:
|
|
498
|
+
ret.append(schema[pre_idx:idx])
|
|
499
|
+
pre_idx = idx + 1
|
|
500
|
+
elif schema[idx] == '"':
|
|
501
|
+
in_quote = True
|
|
502
|
+
pre_idx = idx + 1
|
|
503
|
+
else:
|
|
504
|
+
if schema[idx] == '"' and pre_idx < idx:
|
|
505
|
+
ret.append(schema[pre_idx:idx])
|
|
506
|
+
in_quote = False
|
|
507
|
+
pre_idx = idx + 1
|
|
508
|
+
idx += 1
|
|
509
|
+
if pre_idx < len(schema) and schema[pre_idx] == ".":
|
|
510
|
+
pre_idx += 1
|
|
511
|
+
if pre_idx < idx:
|
|
512
|
+
ret.append(schema[pre_idx:idx])
|
|
513
|
+
|
|
514
|
+
# convert the returning strings back to quoted_name types, and assign the original 'quote' attribute on it
|
|
515
|
+
quoted_ret = [
|
|
516
|
+
quoted_name(value, quote=getattr(schema, "quote", None)) for value in ret
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
return quoted_ret
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class SnowflakeCompiler(compiler.SQLCompiler):
|
|
523
|
+
def visit_sequence(self, sequence, **kw):
|
|
524
|
+
return self.dialect.identifier_preparer.format_sequence(sequence) + ".nextval"
|
|
525
|
+
|
|
526
|
+
def visit_now_func(self, now, **kw):
|
|
527
|
+
return "CURRENT_TIMESTAMP"
|
|
528
|
+
|
|
529
|
+
def visit_merge_into(self, merge_into, **kw):
|
|
530
|
+
clauses = " ".join(
|
|
531
|
+
clause._compiler_dispatch(self, **kw) for clause in merge_into.clauses
|
|
532
|
+
)
|
|
533
|
+
return (
|
|
534
|
+
f"MERGE INTO {merge_into.target} USING {merge_into.source} ON {merge_into.on}"
|
|
535
|
+
+ (" " + clauses if clauses else "")
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def visit_merge_into_clause(self, merge_into_clause, **kw):
|
|
539
|
+
case_predicate = (
|
|
540
|
+
f" AND {str(merge_into_clause.predicate._compiler_dispatch(self, **kw))}"
|
|
541
|
+
if merge_into_clause.predicate is not None
|
|
542
|
+
else ""
|
|
543
|
+
)
|
|
544
|
+
if merge_into_clause.command == "INSERT":
|
|
545
|
+
sets, sets_tos = zip(*merge_into_clause.set.items())
|
|
546
|
+
sets, sets_tos = list(sets), list(sets_tos)
|
|
547
|
+
if kw.get("deterministic", False):
|
|
548
|
+
sets, sets_tos = zip(
|
|
549
|
+
*sorted(merge_into_clause.set.items(), key=operator.itemgetter(0))
|
|
550
|
+
)
|
|
551
|
+
return "WHEN NOT MATCHED{} THEN {} ({}) VALUES ({})".format(
|
|
552
|
+
case_predicate,
|
|
553
|
+
merge_into_clause.command,
|
|
554
|
+
", ".join(sets),
|
|
555
|
+
", ".join(map(lambda e: e._compiler_dispatch(self, **kw), sets_tos)),
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
set_list = list(merge_into_clause.set.items())
|
|
559
|
+
if kw.get("deterministic", False):
|
|
560
|
+
set_list.sort(key=operator.itemgetter(0))
|
|
561
|
+
sets = (
|
|
562
|
+
", ".join(
|
|
563
|
+
[
|
|
564
|
+
f"{set[0]} = {set[1]._compiler_dispatch(self, **kw)}"
|
|
565
|
+
for set in set_list
|
|
566
|
+
]
|
|
567
|
+
)
|
|
568
|
+
if merge_into_clause.set
|
|
569
|
+
else ""
|
|
570
|
+
)
|
|
571
|
+
return "WHEN MATCHED{} THEN {}{}".format(
|
|
572
|
+
case_predicate,
|
|
573
|
+
merge_into_clause.command,
|
|
574
|
+
" SET %s" % sets if merge_into_clause.set else "",
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
def visit_copy_into(self, copy_into, **kw):
|
|
578
|
+
if hasattr(copy_into, "formatter") and copy_into.formatter is not None:
|
|
579
|
+
formatter = copy_into.formatter._compiler_dispatch(self, **kw)
|
|
580
|
+
else:
|
|
581
|
+
formatter = ""
|
|
582
|
+
into = (
|
|
583
|
+
copy_into.into
|
|
584
|
+
if isinstance(copy_into.into, Table)
|
|
585
|
+
else copy_into.into._compiler_dispatch(self, **kw)
|
|
586
|
+
)
|
|
587
|
+
if isinstance(copy_into.from_, Table):
|
|
588
|
+
from_ = copy_into.from_.name
|
|
589
|
+
# this is intended to catch AWSBucket and AzureContainer
|
|
590
|
+
elif (
|
|
591
|
+
isinstance(copy_into.from_, AWSBucket)
|
|
592
|
+
or isinstance(copy_into.from_, AzureContainer)
|
|
593
|
+
or isinstance(copy_into.from_, ExternalStage)
|
|
594
|
+
):
|
|
595
|
+
from_ = copy_into.from_._compiler_dispatch(self, **kw)
|
|
596
|
+
# everything else (selects, etc.)
|
|
597
|
+
else:
|
|
598
|
+
from_ = f"({copy_into.from_._compiler_dispatch(self, **kw)})"
|
|
599
|
+
|
|
600
|
+
partition_by_value = None
|
|
601
|
+
if isinstance(copy_into.partition_by, (BindParameter, Executable)):
|
|
602
|
+
partition_by_value = copy_into.partition_by.compile(
|
|
603
|
+
compile_kwargs={"literal_binds": True}
|
|
604
|
+
)
|
|
605
|
+
elif copy_into.partition_by is not None:
|
|
606
|
+
partition_by_value = copy_into.partition_by
|
|
607
|
+
|
|
608
|
+
partition_by = (
|
|
609
|
+
f"PARTITION BY {partition_by_value}"
|
|
610
|
+
if partition_by_value is not None and partition_by_value != ""
|
|
611
|
+
else ""
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
credentials, encryption = "", ""
|
|
615
|
+
if isinstance(into, tuple):
|
|
616
|
+
into, credentials, encryption = into
|
|
617
|
+
elif isinstance(from_, tuple):
|
|
618
|
+
from_, credentials, encryption = from_
|
|
619
|
+
options_list = list(copy_into.copy_options.items())
|
|
620
|
+
if kw.get("deterministic", False):
|
|
621
|
+
options_list.sort(key=operator.itemgetter(0))
|
|
622
|
+
options = (
|
|
623
|
+
(
|
|
624
|
+
" ".join(
|
|
625
|
+
[
|
|
626
|
+
"{} = {}".format(
|
|
627
|
+
n,
|
|
628
|
+
(
|
|
629
|
+
v._compiler_dispatch(self, **kw)
|
|
630
|
+
if getattr(v, "compiler_dispatch", False)
|
|
631
|
+
else str(v)
|
|
632
|
+
),
|
|
633
|
+
)
|
|
634
|
+
for n, v in options_list
|
|
635
|
+
]
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
if copy_into.copy_options
|
|
639
|
+
else ""
|
|
640
|
+
)
|
|
641
|
+
if credentials:
|
|
642
|
+
options += f" {credentials}"
|
|
643
|
+
if encryption:
|
|
644
|
+
options += f" {encryption}"
|
|
645
|
+
return f"COPY INTO {into} FROM {' '.join([from_, partition_by, formatter, options])}"
|
|
646
|
+
|
|
647
|
+
def visit_copy_formatter(self, formatter, **kw):
|
|
648
|
+
options_list = list(formatter.options.items())
|
|
649
|
+
if kw.get("deterministic", False):
|
|
650
|
+
options_list.sort(key=operator.itemgetter(0))
|
|
651
|
+
if "format_name" in formatter.options:
|
|
652
|
+
return f"FILE_FORMAT=(format_name = {formatter.options['format_name']})"
|
|
653
|
+
return "FILE_FORMAT=(TYPE={}{})".format(
|
|
654
|
+
formatter.file_format,
|
|
655
|
+
(
|
|
656
|
+
" "
|
|
657
|
+
+ " ".join(
|
|
658
|
+
[
|
|
659
|
+
"{}={}".format(
|
|
660
|
+
name,
|
|
661
|
+
(
|
|
662
|
+
value._compiler_dispatch(self, **kw)
|
|
663
|
+
if hasattr(value, "_compiler_dispatch")
|
|
664
|
+
else formatter.value_repr(name, value)
|
|
665
|
+
),
|
|
666
|
+
)
|
|
667
|
+
for name, value in options_list
|
|
668
|
+
]
|
|
669
|
+
)
|
|
670
|
+
if formatter.options
|
|
671
|
+
else ""
|
|
672
|
+
),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def visit_aws_bucket(self, aws_bucket, **kw):
|
|
676
|
+
credentials_list = list(aws_bucket.credentials_used.items())
|
|
677
|
+
if kw.get("deterministic", False):
|
|
678
|
+
credentials_list.sort(key=operator.itemgetter(0))
|
|
679
|
+
credentials = "CREDENTIALS=({})".format(
|
|
680
|
+
" ".join(f"{n}='{v}'" for n, v in credentials_list)
|
|
681
|
+
)
|
|
682
|
+
encryption_list = list(aws_bucket.encryption_used.items())
|
|
683
|
+
if kw.get("deterministic", False):
|
|
684
|
+
encryption_list.sort(key=operator.itemgetter(0))
|
|
685
|
+
encryption = "ENCRYPTION=({})".format(
|
|
686
|
+
" ".join(
|
|
687
|
+
("{}='{}'" if isinstance(v, string_types) else "{}={}").format(n, v)
|
|
688
|
+
for n, v in encryption_list
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
uri = "'s3://{}{}'".format(
|
|
692
|
+
aws_bucket.bucket, f"/{aws_bucket.path}" if aws_bucket.path else ""
|
|
693
|
+
)
|
|
694
|
+
return (
|
|
695
|
+
uri,
|
|
696
|
+
credentials if aws_bucket.credentials_used else "",
|
|
697
|
+
encryption if aws_bucket.encryption_used else "",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def visit_azure_container(self, azure_container, **kw):
|
|
701
|
+
credentials_list = list(azure_container.credentials_used.items())
|
|
702
|
+
if kw.get("deterministic", False):
|
|
703
|
+
credentials_list.sort(key=operator.itemgetter(0))
|
|
704
|
+
credentials = "CREDENTIALS=({})".format(
|
|
705
|
+
" ".join(f"{n}='{v}'" for n, v in credentials_list)
|
|
706
|
+
)
|
|
707
|
+
encryption_list = list(azure_container.encryption_used.items())
|
|
708
|
+
if kw.get("deterministic", False):
|
|
709
|
+
encryption_list.sort(key=operator.itemgetter(0))
|
|
710
|
+
encryption = "ENCRYPTION=({})".format(
|
|
711
|
+
" ".join(
|
|
712
|
+
f"{n}='{v}'" if isinstance(v, string_types) else f"{n}={v}"
|
|
713
|
+
for n, v in encryption_list
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
uri = "'azure://{}.blob.core.windows.net/{}{}'".format(
|
|
717
|
+
azure_container.account,
|
|
718
|
+
azure_container.container,
|
|
719
|
+
f"/{azure_container.path}" if azure_container.path else "",
|
|
720
|
+
)
|
|
721
|
+
return (
|
|
722
|
+
uri,
|
|
723
|
+
credentials if azure_container.credentials_used else "",
|
|
724
|
+
encryption if azure_container.encryption_used else "",
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
def visit_external_stage(self, external_stage, **kw):
|
|
728
|
+
if external_stage.file_format is None:
|
|
729
|
+
return (
|
|
730
|
+
f"@{external_stage.namespace}{external_stage.name}{external_stage.path}"
|
|
731
|
+
)
|
|
732
|
+
return f"@{external_stage.namespace}{external_stage.name}{external_stage.path} (file_format => {external_stage.file_format})"
|
|
733
|
+
|
|
734
|
+
def delete_extra_from_clause(
|
|
735
|
+
self, delete_stmt, from_table, extra_froms, from_hints, **kw
|
|
736
|
+
):
|
|
737
|
+
return "USING " + ", ".join(
|
|
738
|
+
t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
|
|
739
|
+
for t in extra_froms
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def update_from_clause(
|
|
743
|
+
self, update_stmt, from_table, extra_froms, from_hints, **kw
|
|
744
|
+
):
|
|
745
|
+
return "FROM " + ", ".join(
|
|
746
|
+
t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
|
|
747
|
+
for t in extra_froms
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def _get_regexp_args(self, binary, kw):
|
|
751
|
+
string = self.process(binary.left, **kw)
|
|
752
|
+
pattern = self.process(binary.right, **kw)
|
|
753
|
+
flags = binary.modifiers["flags"]
|
|
754
|
+
if flags is not None:
|
|
755
|
+
flags = self.process(flags, **kw)
|
|
756
|
+
return string, pattern, flags
|
|
757
|
+
|
|
758
|
+
def visit_regexp_match_op_binary(self, binary, operator, **kw):
|
|
759
|
+
string, pattern, flags = self._get_regexp_args(binary, kw)
|
|
760
|
+
if flags is None:
|
|
761
|
+
return f"REGEXP_LIKE({string}, {pattern})"
|
|
762
|
+
else:
|
|
763
|
+
return f"REGEXP_LIKE({string}, {pattern}, {flags})"
|
|
764
|
+
|
|
765
|
+
def visit_regexp_replace_op_binary(self, binary, operator, **kw):
|
|
766
|
+
string, pattern, flags = self._get_regexp_args(binary, kw)
|
|
767
|
+
try:
|
|
768
|
+
replacement = self.process(binary.modifiers["replacement"], **kw)
|
|
769
|
+
except KeyError:
|
|
770
|
+
# in sqlalchemy 1.4.49, the internal structure of the expression is changed
|
|
771
|
+
# that binary.modifiers doesn't have "replacement":
|
|
772
|
+
# https://docs.sqlalchemy.org/en/20/changelog/changelog_14.html#change-1.4.49
|
|
773
|
+
return f"REGEXP_REPLACE({string}, {pattern}{'' if flags is None else f', {flags}'})"
|
|
774
|
+
|
|
775
|
+
if flags is None:
|
|
776
|
+
return f"REGEXP_REPLACE({string}, {pattern}, {replacement})"
|
|
777
|
+
else:
|
|
778
|
+
return f"REGEXP_REPLACE({string}, {pattern}, {replacement}, {flags})"
|
|
779
|
+
|
|
780
|
+
def visit_not_regexp_match_op_binary(self, binary, operator, **kw):
|
|
781
|
+
return f"NOT {self.visit_regexp_match_op_binary(binary, operator, **kw)}"
|
|
782
|
+
|
|
783
|
+
def visit_join(self, join, asfrom=False, from_linter=None, **kwargs):
|
|
784
|
+
if from_linter:
|
|
785
|
+
from_linter.edges.update(
|
|
786
|
+
itertools.product(join.left._from_objects, join.right._from_objects)
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
if join.full:
|
|
790
|
+
join_type = " FULL OUTER JOIN "
|
|
791
|
+
elif join.isouter:
|
|
792
|
+
join_type = " LEFT OUTER JOIN "
|
|
793
|
+
else:
|
|
794
|
+
join_type = " JOIN "
|
|
795
|
+
|
|
796
|
+
join_statement = (
|
|
797
|
+
join.left._compiler_dispatch(
|
|
798
|
+
self, asfrom=True, from_linter=from_linter, **kwargs
|
|
799
|
+
)
|
|
800
|
+
+ join_type
|
|
801
|
+
+ join.right._compiler_dispatch(
|
|
802
|
+
self, asfrom=True, from_linter=from_linter, **kwargs
|
|
803
|
+
)
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
if join.onclause is None and isinstance(join.right, Lateral):
|
|
807
|
+
# in snowflake, onclause is not accepted for lateral due to BCR change:
|
|
808
|
+
# https://docs.snowflake.com/en/release-notes/bcr-bundles/2023_04/bcr-1057
|
|
809
|
+
# sqlalchemy only allows join with on condition.
|
|
810
|
+
# to adapt to snowflake syntax change,
|
|
811
|
+
# we make the change such that when oncaluse is None and the right part is
|
|
812
|
+
# Lateral, we do not append the on condition
|
|
813
|
+
return join_statement
|
|
814
|
+
|
|
815
|
+
return (
|
|
816
|
+
join_statement
|
|
817
|
+
+ " ON "
|
|
818
|
+
# TODO: likely need asfrom=True here?
|
|
819
|
+
+ join.onclause._compiler_dispatch(self, from_linter=from_linter, **kwargs)
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
def visit_truediv_binary(self, binary, operator, **kw):
|
|
823
|
+
if self.dialect.div_is_floordiv:
|
|
824
|
+
warnings.warn(
|
|
825
|
+
"div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division",
|
|
826
|
+
PendingDeprecationWarning,
|
|
827
|
+
stacklevel=2,
|
|
828
|
+
)
|
|
829
|
+
return (
|
|
830
|
+
self.process(binary.left, **kw) + " / " + self.process(binary.right, **kw)
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
def visit_floordiv_binary(self, binary, operator, **kw):
|
|
834
|
+
if self.dialect.div_is_floordiv and IS_VERSION_20:
|
|
835
|
+
warnings.warn(
|
|
836
|
+
"div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division",
|
|
837
|
+
PendingDeprecationWarning,
|
|
838
|
+
stacklevel=2,
|
|
839
|
+
)
|
|
840
|
+
return super().visit_floordiv_binary(binary, operator, **kw)
|
|
841
|
+
|
|
842
|
+
def render_literal_value(self, value, type_):
|
|
843
|
+
# escape backslash
|
|
844
|
+
return super().render_literal_value(value, type_).replace("\\", "\\\\")
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class SnowflakeExecutionContext(default.DefaultExecutionContext):
|
|
848
|
+
INSERT_SQL_RE = re.compile(r"^insert\s+into", flags=re.IGNORECASE)
|
|
849
|
+
|
|
850
|
+
def fire_sequence(self, seq, type_):
|
|
851
|
+
return self._execute_scalar(
|
|
852
|
+
f"SELECT {self.identifier_preparer.format_sequence(seq)}.nextval",
|
|
853
|
+
type_,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def should_autocommit_text(self, statement):
|
|
857
|
+
return AUTOCOMMIT_REGEXP.match(statement)
|
|
858
|
+
|
|
859
|
+
@sa_util.memoized_property
|
|
860
|
+
def should_autocommit(self):
|
|
861
|
+
autocommit = self.execution_options.get(
|
|
862
|
+
"autocommit",
|
|
863
|
+
not self.compiled
|
|
864
|
+
and self.statement
|
|
865
|
+
and expression.PARSE_AUTOCOMMIT
|
|
866
|
+
or False,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
if autocommit is expression.PARSE_AUTOCOMMIT:
|
|
870
|
+
return self.should_autocommit_text(self.unicode_statement)
|
|
871
|
+
else:
|
|
872
|
+
return autocommit and not self.isddl
|
|
873
|
+
|
|
874
|
+
def pre_exec(self):
|
|
875
|
+
if self.compiled and self.identifier_preparer._double_percents:
|
|
876
|
+
# for compiled statements, percent is doubled for escape, we turn on _interpolate_empty_sequences
|
|
877
|
+
_set_connection_interpolate_empty_sequences(self._dbapi_connection, True)
|
|
878
|
+
|
|
879
|
+
# if the statement is executemany insert, setting _interpolate_empty_sequences to True is not enough,
|
|
880
|
+
# because executemany pre-processes the param binding and then pass None params to execute so
|
|
881
|
+
# _interpolate_empty_sequences condition not getting met for the command.
|
|
882
|
+
# Therefore, we manually revert the escape percent in the command here
|
|
883
|
+
if self.executemany and self.INSERT_SQL_RE.match(self.statement):
|
|
884
|
+
self.statement = self.statement.replace("%%", "%")
|
|
885
|
+
else:
|
|
886
|
+
# for other cases, do no interpolate empty sequences as "%" is not double escaped
|
|
887
|
+
_set_connection_interpolate_empty_sequences(self._dbapi_connection, False)
|
|
888
|
+
|
|
889
|
+
def post_exec(self):
|
|
890
|
+
if self.compiled and self.identifier_preparer._double_percents:
|
|
891
|
+
# for compiled statements, percent is doubled for escapeafter execution
|
|
892
|
+
# we reset _interpolate_empty_sequences to false which is turned on in pre_exec
|
|
893
|
+
_set_connection_interpolate_empty_sequences(self._dbapi_connection, False)
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def rowcount(self):
|
|
897
|
+
return self.cursor.rowcount
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
class SnowflakeDDLCompiler(compiler.DDLCompiler):
|
|
901
|
+
def denormalize_column_name(self, name):
|
|
902
|
+
if name is None:
|
|
903
|
+
return None
|
|
904
|
+
elif name.lower() == name and not self.preparer._requires_quotes(name.lower()):
|
|
905
|
+
# no quote as case insensitive
|
|
906
|
+
return name
|
|
907
|
+
return self.preparer.quote(name)
|
|
908
|
+
|
|
909
|
+
def get_column_specification(self, column, **kwargs):
|
|
910
|
+
"""
|
|
911
|
+
Gets Column specifications
|
|
912
|
+
"""
|
|
913
|
+
colspec = [
|
|
914
|
+
self.preparer.format_column(column),
|
|
915
|
+
self.dialect.type_compiler.process(column.type, type_expression=column),
|
|
916
|
+
]
|
|
917
|
+
|
|
918
|
+
has_identity = (
|
|
919
|
+
column.identity is not None and self.dialect.supports_identity_columns
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
if not column.nullable:
|
|
923
|
+
colspec.append("NOT NULL")
|
|
924
|
+
|
|
925
|
+
default = self.get_column_default_string(column)
|
|
926
|
+
if default is not None:
|
|
927
|
+
colspec.append("DEFAULT " + default)
|
|
928
|
+
|
|
929
|
+
# TODO: This makes the first INTEGER column AUTOINCREMENT.
|
|
930
|
+
# But the column is not really considered so unless
|
|
931
|
+
# postfetch_lastrowid is enabled. But it is very unlikely to happen...
|
|
932
|
+
if (
|
|
933
|
+
column.table is not None
|
|
934
|
+
and column is column.table._autoincrement_column
|
|
935
|
+
and column.server_default is None
|
|
936
|
+
):
|
|
937
|
+
if isinstance(column.default, Sequence):
|
|
938
|
+
colspec.append(
|
|
939
|
+
f"DEFAULT {self.dialect.identifier_preparer.format_sequence(column.default)}.nextval"
|
|
940
|
+
)
|
|
941
|
+
else:
|
|
942
|
+
colspec.append("AUTOINCREMENT")
|
|
943
|
+
|
|
944
|
+
if has_identity:
|
|
945
|
+
colspec.append(self.process(column.identity))
|
|
946
|
+
|
|
947
|
+
return " ".join(colspec)
|
|
948
|
+
|
|
949
|
+
def handle_cluster_by(self, table):
|
|
950
|
+
"""
|
|
951
|
+
Handles snowflake-specific ``CREATE TABLE ... CLUSTER BY`` syntax.
|
|
952
|
+
|
|
953
|
+
Users can specify the `clusterby` property per table
|
|
954
|
+
using the dialect specific syntax.
|
|
955
|
+
For example, to specify a cluster by key you apply the following:
|
|
956
|
+
|
|
957
|
+
>>> import sqlalchemy as sa
|
|
958
|
+
>>> from sqlalchemy.schema import CreateTable
|
|
959
|
+
>>> engine = sa.create_engine('snowflake://om1')
|
|
960
|
+
>>> metadata = sa.MetaData()
|
|
961
|
+
>>> user = sa.Table(
|
|
962
|
+
... 'user',
|
|
963
|
+
... metadata,
|
|
964
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
965
|
+
... sa.Column('name', sa.String),
|
|
966
|
+
... snowflake_clusterby=['id', 'name', text("id > 5")]
|
|
967
|
+
... )
|
|
968
|
+
>>> print(CreateTable(user).compile(engine))
|
|
969
|
+
<BLANKLINE>
|
|
970
|
+
CREATE TABLE "user" (
|
|
971
|
+
id INTEGER NOT NULL AUTOINCREMENT,
|
|
972
|
+
name VARCHAR,
|
|
973
|
+
PRIMARY KEY (id)
|
|
974
|
+
) CLUSTER BY (id, name, id > 5)
|
|
975
|
+
<BLANKLINE>
|
|
976
|
+
<BLANKLINE>
|
|
977
|
+
"""
|
|
978
|
+
text = ""
|
|
979
|
+
info = table.dialect_options[DIALECT_NAME]
|
|
980
|
+
cluster = info.get("clusterby")
|
|
981
|
+
if cluster:
|
|
982
|
+
text += " CLUSTER BY ({})".format(
|
|
983
|
+
", ".join(
|
|
984
|
+
(
|
|
985
|
+
self.denormalize_column_name(key)
|
|
986
|
+
if isinstance(key, str)
|
|
987
|
+
else str(key)
|
|
988
|
+
)
|
|
989
|
+
for key in cluster
|
|
990
|
+
)
|
|
991
|
+
)
|
|
992
|
+
return text
|
|
993
|
+
|
|
994
|
+
def post_create_table(self, table):
|
|
995
|
+
text = self.handle_cluster_by(table)
|
|
996
|
+
options = []
|
|
997
|
+
invalid_options: List[str] = []
|
|
998
|
+
|
|
999
|
+
for key, option in table.dialect_options[DIALECT_NAME].items():
|
|
1000
|
+
if isinstance(option, TableOption):
|
|
1001
|
+
options.append(option)
|
|
1002
|
+
elif key not in ["clusterby", "*"]:
|
|
1003
|
+
invalid_options.append(key)
|
|
1004
|
+
|
|
1005
|
+
if len(invalid_options) > 0:
|
|
1006
|
+
raise UnexpectedOptionTypeError(sorted(invalid_options))
|
|
1007
|
+
|
|
1008
|
+
if isinstance(table, CustomTableBase):
|
|
1009
|
+
options.sort(key=lambda x: (x.priority.value, x.option_name), reverse=True)
|
|
1010
|
+
for option in options:
|
|
1011
|
+
text += "\t" + option.render_option(self)
|
|
1012
|
+
elif len(options) > 0:
|
|
1013
|
+
raise CustomOptionsAreOnlySupportedOnSnowflakeTables()
|
|
1014
|
+
|
|
1015
|
+
return text
|
|
1016
|
+
|
|
1017
|
+
def visit_create_stage(self, create_stage, **kw):
|
|
1018
|
+
"""
|
|
1019
|
+
This visitor will create the SQL representation for a CREATE STAGE command.
|
|
1020
|
+
"""
|
|
1021
|
+
return "CREATE {or_replace}{temporary}STAGE {}{} URL={}".format(
|
|
1022
|
+
create_stage.stage.namespace,
|
|
1023
|
+
create_stage.stage.name,
|
|
1024
|
+
repr(create_stage.container),
|
|
1025
|
+
or_replace="OR REPLACE " if create_stage.replace_if_exists else "",
|
|
1026
|
+
temporary="TEMPORARY " if create_stage.temporary else "",
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
def visit_create_file_format(self, file_format, **kw):
|
|
1030
|
+
"""
|
|
1031
|
+
This visitor will create the SQL representation for a CREATE FILE FORMAT
|
|
1032
|
+
command.
|
|
1033
|
+
"""
|
|
1034
|
+
return "CREATE {}FILE FORMAT {} TYPE='{}' {}".format(
|
|
1035
|
+
"OR REPLACE " if file_format.replace_if_exists else "",
|
|
1036
|
+
file_format.format_name,
|
|
1037
|
+
file_format.formatter.file_format,
|
|
1038
|
+
" ".join(
|
|
1039
|
+
[
|
|
1040
|
+
f"{name} = {file_format.formatter.value_repr(name, value)}"
|
|
1041
|
+
for name, value in file_format.formatter.options.items()
|
|
1042
|
+
]
|
|
1043
|
+
),
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
def visit_drop_table_comment(self, drop, **kw):
|
|
1047
|
+
"""Snowflake does not support setting table comments as NULL.
|
|
1048
|
+
|
|
1049
|
+
Reflection has to account for this and convert any empty comments to NULL.
|
|
1050
|
+
"""
|
|
1051
|
+
table_name = self.preparer.format_table(drop.element)
|
|
1052
|
+
return f"COMMENT ON TABLE {table_name} IS ''"
|
|
1053
|
+
|
|
1054
|
+
def visit_drop_column_comment(self, drop, **kw):
|
|
1055
|
+
"""Snowflake does not support directly setting column comments as NULL.
|
|
1056
|
+
|
|
1057
|
+
Instead we are forced to use the ALTER COLUMN ... UNSET COMMENT instead.
|
|
1058
|
+
"""
|
|
1059
|
+
return "ALTER TABLE {} ALTER COLUMN {} UNSET COMMENT".format(
|
|
1060
|
+
self.preparer.format_table(drop.element.table),
|
|
1061
|
+
self.preparer.format_column(drop.element),
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
def visit_identity_column(self, identity, **kw):
|
|
1065
|
+
text = "IDENTITY"
|
|
1066
|
+
if identity.start is not None or identity.increment is not None:
|
|
1067
|
+
start = 1 if identity.start is None else identity.start
|
|
1068
|
+
increment = 1 if identity.increment is None else identity.increment
|
|
1069
|
+
text += f"({start},{increment})"
|
|
1070
|
+
if identity.order is not None:
|
|
1071
|
+
order = "ORDER" if identity.order else "NOORDER"
|
|
1072
|
+
text += f" {order}"
|
|
1073
|
+
return text
|
|
1074
|
+
|
|
1075
|
+
def get_identity_options(self, identity_options):
|
|
1076
|
+
text = []
|
|
1077
|
+
if identity_options.increment is not None:
|
|
1078
|
+
text.append("INCREMENT BY %d" % identity_options.increment)
|
|
1079
|
+
if identity_options.start is not None:
|
|
1080
|
+
text.append("START WITH %d" % identity_options.start)
|
|
1081
|
+
if identity_options.minvalue is not None:
|
|
1082
|
+
text.append("MINVALUE %d" % identity_options.minvalue)
|
|
1083
|
+
if identity_options.maxvalue is not None:
|
|
1084
|
+
text.append("MAXVALUE %d" % identity_options.maxvalue)
|
|
1085
|
+
if identity_options.nominvalue is not None:
|
|
1086
|
+
text.append("NO MINVALUE")
|
|
1087
|
+
if identity_options.nomaxvalue is not None:
|
|
1088
|
+
text.append("NO MAXVALUE")
|
|
1089
|
+
if identity_options.cache is not None:
|
|
1090
|
+
text.append("CACHE %d" % identity_options.cache)
|
|
1091
|
+
if identity_options.cycle is not None:
|
|
1092
|
+
text.append("CYCLE" if identity_options.cycle else "NO CYCLE")
|
|
1093
|
+
if identity_options.order is not None:
|
|
1094
|
+
text.append("ORDER" if identity_options.order else "NOORDER")
|
|
1095
|
+
return " ".join(text)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
|
|
1099
|
+
def visit_BYTEINT(self, type_, **kw):
|
|
1100
|
+
return "BYTEINT"
|
|
1101
|
+
|
|
1102
|
+
def visit_CHARACTER(self, type_, **kw):
|
|
1103
|
+
return "CHARACTER"
|
|
1104
|
+
|
|
1105
|
+
def visit_DEC(self, type_, **kw):
|
|
1106
|
+
return "DEC"
|
|
1107
|
+
|
|
1108
|
+
def visit_DOUBLE(self, type_, **kw):
|
|
1109
|
+
return "DOUBLE"
|
|
1110
|
+
|
|
1111
|
+
def visit_FIXED(self, type_, **kw):
|
|
1112
|
+
return "FIXED"
|
|
1113
|
+
|
|
1114
|
+
def visit_INT(self, type_, **kw):
|
|
1115
|
+
return "INT"
|
|
1116
|
+
|
|
1117
|
+
def visit_NUMBER(self, type_, **kw):
|
|
1118
|
+
return "NUMBER"
|
|
1119
|
+
|
|
1120
|
+
def visit_STRING(self, type_, **kw):
|
|
1121
|
+
return "STRING"
|
|
1122
|
+
|
|
1123
|
+
def visit_TINYINT(self, type_, **kw):
|
|
1124
|
+
return "TINYINT"
|
|
1125
|
+
|
|
1126
|
+
def visit_VARIANT(self, type_, **kw):
|
|
1127
|
+
return "VARIANT"
|
|
1128
|
+
|
|
1129
|
+
def visit_MAP(self, type_, **kw):
|
|
1130
|
+
not_null = f" {NOT_NULL}" if type_.not_null else ""
|
|
1131
|
+
return (
|
|
1132
|
+
f"MAP({type_.key_type.compile()}, {type_.value_type.compile()}{not_null})"
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
def visit_ARRAY(self, type_, **kw):
|
|
1136
|
+
return "ARRAY"
|
|
1137
|
+
|
|
1138
|
+
def visit_SNOWFLAKE_ARRAY(self, type_, **kw):
|
|
1139
|
+
if type_.is_semi_structured:
|
|
1140
|
+
return "ARRAY"
|
|
1141
|
+
not_null = f" {NOT_NULL}" if type_.not_null else ""
|
|
1142
|
+
return f"ARRAY({type_.value_type.compile()}{not_null})"
|
|
1143
|
+
|
|
1144
|
+
def visit_OBJECT(self, type_, **kw):
|
|
1145
|
+
if type_.is_semi_structured:
|
|
1146
|
+
return "OBJECT"
|
|
1147
|
+
else:
|
|
1148
|
+
contents = []
|
|
1149
|
+
for key in type_.items_types:
|
|
1150
|
+
|
|
1151
|
+
row_text = f"{key} {type_.items_types[key][0].compile()}"
|
|
1152
|
+
# Type and not null is specified
|
|
1153
|
+
if len(type_.items_types[key]) > 1:
|
|
1154
|
+
row_text += f"{' NOT NULL' if type_.items_types[key][1] else ''}"
|
|
1155
|
+
contents.append(row_text)
|
|
1156
|
+
return "OBJECT" if contents == [] else f"OBJECT({', '.join(contents)})"
|
|
1157
|
+
|
|
1158
|
+
def visit_BLOB(self, type_, **kw):
|
|
1159
|
+
return "BINARY"
|
|
1160
|
+
|
|
1161
|
+
def visit_datetime(self, type_, **kw):
|
|
1162
|
+
return "datetime"
|
|
1163
|
+
|
|
1164
|
+
def visit_DATETIME(self, type_, **kw):
|
|
1165
|
+
return "DATETIME"
|
|
1166
|
+
|
|
1167
|
+
def visit_TIMESTAMP_NTZ(self, type_, **kw):
|
|
1168
|
+
return "TIMESTAMP_NTZ"
|
|
1169
|
+
|
|
1170
|
+
def visit_TIMESTAMP_TZ(self, type_, **kw):
|
|
1171
|
+
return "TIMESTAMP_TZ"
|
|
1172
|
+
|
|
1173
|
+
def visit_TIMESTAMP_LTZ(self, type_, **kw):
|
|
1174
|
+
return "TIMESTAMP_LTZ"
|
|
1175
|
+
|
|
1176
|
+
def visit_TIMESTAMP(self, type_, **kw):
|
|
1177
|
+
return "TIMESTAMP"
|
|
1178
|
+
|
|
1179
|
+
def visit_GEOGRAPHY(self, type_, **kw):
|
|
1180
|
+
return "GEOGRAPHY"
|
|
1181
|
+
|
|
1182
|
+
def visit_GEOMETRY(self, type_, **kw):
|
|
1183
|
+
return "GEOMETRY"
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
construct_arguments = [(Table, {"clusterby": None})]
|
|
1187
|
+
|
|
1188
|
+
functions.register_function("flatten", flatten, "snowflake")
|