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.
Files changed (39) hide show
  1. snowflake/sqlalchemy/__init__.py +162 -0
  2. snowflake/sqlalchemy/_constants.py +14 -0
  3. snowflake/sqlalchemy/base.py +1188 -0
  4. snowflake/sqlalchemy/compat.py +36 -0
  5. snowflake/sqlalchemy/custom_commands.py +627 -0
  6. snowflake/sqlalchemy/custom_types.py +155 -0
  7. snowflake/sqlalchemy/exc.py +82 -0
  8. snowflake/sqlalchemy/functions.py +16 -0
  9. snowflake/sqlalchemy/parser/custom_type_parser.py +245 -0
  10. snowflake/sqlalchemy/provision.py +12 -0
  11. snowflake/sqlalchemy/requirements.py +313 -0
  12. snowflake/sqlalchemy/snowdialect.py +1029 -0
  13. snowflake/sqlalchemy/sql/__init__.py +3 -0
  14. snowflake/sqlalchemy/sql/custom_schema/__init__.py +9 -0
  15. snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +37 -0
  16. snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +127 -0
  17. snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +13 -0
  18. snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +117 -0
  19. snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +63 -0
  20. snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +102 -0
  21. snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +33 -0
  22. snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +63 -0
  23. snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +58 -0
  24. snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +63 -0
  25. snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +25 -0
  26. snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +65 -0
  27. snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +14 -0
  28. snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +67 -0
  29. snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +84 -0
  30. snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +94 -0
  31. snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +70 -0
  32. snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +54 -0
  33. snowflake/sqlalchemy/util.py +344 -0
  34. snowflake/sqlalchemy/version.py +6 -0
  35. snowflake_sqlalchemy-1.7.3.dist-info/METADATA +737 -0
  36. snowflake_sqlalchemy-1.7.3.dist-info/RECORD +39 -0
  37. snowflake_sqlalchemy-1.7.3.dist-info/WHEEL +4 -0
  38. snowflake_sqlalchemy-1.7.3.dist-info/entry_points.txt +2 -0
  39. 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")