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,344 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from itertools import chain
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote_plus
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import exc, inspection, sql
|
|
11
|
+
from sqlalchemy.exc import NoForeignKeysError
|
|
12
|
+
from sqlalchemy.orm.interfaces import MapperProperty
|
|
13
|
+
from sqlalchemy.orm.util import _ORMJoin as sa_orm_util_ORMJoin
|
|
14
|
+
from sqlalchemy.orm.util import attributes
|
|
15
|
+
from sqlalchemy.sql import util as sql_util
|
|
16
|
+
from sqlalchemy.sql.base import _expand_cloned, _from_objects
|
|
17
|
+
from sqlalchemy.sql.elements import _find_columns
|
|
18
|
+
from sqlalchemy.sql.selectable import Join, Lateral, coercions, operators, roles
|
|
19
|
+
|
|
20
|
+
from snowflake.connector.compat import IS_STR
|
|
21
|
+
from snowflake.connector.connection import SnowflakeConnection
|
|
22
|
+
from snowflake.sqlalchemy import compat
|
|
23
|
+
|
|
24
|
+
from ._constants import (
|
|
25
|
+
APPLICATION_NAME,
|
|
26
|
+
PARAM_APPLICATION,
|
|
27
|
+
PARAM_INTERNAL_APPLICATION_NAME,
|
|
28
|
+
PARAM_INTERNAL_APPLICATION_VERSION,
|
|
29
|
+
SNOWFLAKE_SQLALCHEMY_VERSION,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _rfc_1738_quote(text):
|
|
34
|
+
return re.sub(r"[:@/]", lambda m: "%%%X" % ord(m.group(0)), text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _url(**db_parameters):
|
|
38
|
+
"""
|
|
39
|
+
Composes a SQLAlchemy connect string from the given database connection
|
|
40
|
+
parameters.
|
|
41
|
+
|
|
42
|
+
Password containing special characters (e.g., '@', '%') need to be encoded to be parsed correctly.
|
|
43
|
+
Unescaped password containing special characters might lead to authentication failure.
|
|
44
|
+
Please follow the instructions to encode the password:
|
|
45
|
+
https://github.com/snowflakedb/snowflake-sqlalchemy#escaping-special-characters-such-as---signs-in-passwords
|
|
46
|
+
"""
|
|
47
|
+
specified_parameters = []
|
|
48
|
+
if "account" not in db_parameters:
|
|
49
|
+
raise exc.ArgumentError("account parameter must be specified.")
|
|
50
|
+
|
|
51
|
+
if "host" in db_parameters:
|
|
52
|
+
ret = "snowflake://{user}:{password}@{host}:{port}/".format(
|
|
53
|
+
user=db_parameters.get("user", ""),
|
|
54
|
+
password=_rfc_1738_quote(db_parameters.get("password", "")),
|
|
55
|
+
host=db_parameters["host"],
|
|
56
|
+
port=db_parameters["port"] if "port" in db_parameters else 443,
|
|
57
|
+
)
|
|
58
|
+
specified_parameters += ["user", "password", "host", "port"]
|
|
59
|
+
elif "region" not in db_parameters:
|
|
60
|
+
ret = "snowflake://{user}:{password}@{account}/".format(
|
|
61
|
+
account=db_parameters["account"],
|
|
62
|
+
user=db_parameters.get("user", ""),
|
|
63
|
+
password=_rfc_1738_quote(db_parameters.get("password", "")),
|
|
64
|
+
)
|
|
65
|
+
specified_parameters += ["user", "password", "account"]
|
|
66
|
+
else:
|
|
67
|
+
ret = "snowflake://{user}:{password}@{account}.{region}/".format(
|
|
68
|
+
account=db_parameters["account"],
|
|
69
|
+
user=db_parameters.get("user", ""),
|
|
70
|
+
password=_rfc_1738_quote(db_parameters.get("password", "")),
|
|
71
|
+
region=db_parameters["region"],
|
|
72
|
+
)
|
|
73
|
+
specified_parameters += ["user", "password", "account", "region"]
|
|
74
|
+
|
|
75
|
+
if "database" in db_parameters:
|
|
76
|
+
ret += quote_plus(db_parameters["database"])
|
|
77
|
+
specified_parameters += ["database"]
|
|
78
|
+
if "schema" in db_parameters:
|
|
79
|
+
ret += "/" + quote_plus(db_parameters["schema"])
|
|
80
|
+
specified_parameters += ["schema"]
|
|
81
|
+
elif "schema" in db_parameters:
|
|
82
|
+
raise exc.ArgumentError("schema cannot be specified without database")
|
|
83
|
+
|
|
84
|
+
def sep(is_first_parameter):
|
|
85
|
+
return "?" if is_first_parameter else "&"
|
|
86
|
+
|
|
87
|
+
is_first_parameter = True
|
|
88
|
+
for p in sorted(db_parameters.keys()):
|
|
89
|
+
v = db_parameters[p]
|
|
90
|
+
if p not in specified_parameters:
|
|
91
|
+
encoded_value = quote_plus(v) if IS_STR(v) else str(v)
|
|
92
|
+
ret += sep(is_first_parameter) + p + "=" + encoded_value
|
|
93
|
+
is_first_parameter = False
|
|
94
|
+
return ret
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _set_connection_interpolate_empty_sequences(
|
|
98
|
+
dbapi_connection: SnowflakeConnection, flag: bool
|
|
99
|
+
) -> None:
|
|
100
|
+
"""set the _interpolate_empty_sequences config of the underlying connection"""
|
|
101
|
+
if hasattr(dbapi_connection, "driver_connection"):
|
|
102
|
+
# _dbapi_connection is a _ConnectionFairy which proxies raw SnowflakeConnection
|
|
103
|
+
dbapi_connection.driver_connection._interpolate_empty_sequences = flag
|
|
104
|
+
else:
|
|
105
|
+
# _dbapi_connection is a raw SnowflakeConnection
|
|
106
|
+
dbapi_connection._interpolate_empty_sequences = flag
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _update_connection_application_name(**conn_kwargs: Any) -> Any:
|
|
110
|
+
if PARAM_APPLICATION not in conn_kwargs:
|
|
111
|
+
conn_kwargs[PARAM_APPLICATION] = APPLICATION_NAME
|
|
112
|
+
if PARAM_INTERNAL_APPLICATION_NAME not in conn_kwargs:
|
|
113
|
+
conn_kwargs[PARAM_INTERNAL_APPLICATION_NAME] = APPLICATION_NAME
|
|
114
|
+
if PARAM_INTERNAL_APPLICATION_VERSION not in conn_kwargs:
|
|
115
|
+
conn_kwargs[PARAM_INTERNAL_APPLICATION_VERSION] = SNOWFLAKE_SQLALCHEMY_VERSION
|
|
116
|
+
return conn_kwargs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_url_boolean(value: str) -> bool:
|
|
120
|
+
if value == "True":
|
|
121
|
+
return True
|
|
122
|
+
elif value == "False":
|
|
123
|
+
return False
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(f"Invalid boolean value detected: '{value}'")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_url_integer(value: str) -> int:
|
|
129
|
+
try:
|
|
130
|
+
return int(value)
|
|
131
|
+
except ValueError as e:
|
|
132
|
+
raise ValueError(f"Invalid int value detected: '{value}") from e
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# handle Snowflake BCR bcr-1057
|
|
136
|
+
# the BCR impacts sqlalchemy.orm.context.ORMSelectCompileState and sqlalchemy.sql.selectable.SelectState
|
|
137
|
+
# which used the 'sqlalchemy.util.preloaded.sql_util.find_left_clause_to_join_from' method that
|
|
138
|
+
# can not handle the BCR change, we implement it in a way that lateral join does not need onclause
|
|
139
|
+
def _find_left_clause_to_join_from(clauses, join_to, onclause):
|
|
140
|
+
"""Given a list of FROM clauses, a selectable,
|
|
141
|
+
and optional ON clause, return a list of integer indexes from the
|
|
142
|
+
clauses list indicating the clauses that can be joined from.
|
|
143
|
+
|
|
144
|
+
The presence of an "onclause" indicates that at least one clause can
|
|
145
|
+
definitely be joined from; if the list of clauses is of length one
|
|
146
|
+
and the onclause is given, returns that index. If the list of clauses
|
|
147
|
+
is more than length one, and the onclause is given, attempts to locate
|
|
148
|
+
which clauses contain the same columns.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
idx = []
|
|
152
|
+
selectables = set(_from_objects(join_to))
|
|
153
|
+
|
|
154
|
+
# if we are given more than one target clause to join
|
|
155
|
+
# from, use the onclause to provide a more specific answer.
|
|
156
|
+
# otherwise, don't try to limit, after all, "ON TRUE" is a valid
|
|
157
|
+
# on clause
|
|
158
|
+
if len(clauses) > 1 and onclause is not None:
|
|
159
|
+
resolve_ambiguity = True
|
|
160
|
+
cols_in_onclause = _find_columns(onclause)
|
|
161
|
+
else:
|
|
162
|
+
resolve_ambiguity = False
|
|
163
|
+
cols_in_onclause = None
|
|
164
|
+
|
|
165
|
+
for i, f in enumerate(clauses):
|
|
166
|
+
for s in selectables.difference([f]):
|
|
167
|
+
if resolve_ambiguity:
|
|
168
|
+
if set(f.c).union(s.c).issuperset(cols_in_onclause):
|
|
169
|
+
idx.append(i)
|
|
170
|
+
break
|
|
171
|
+
elif onclause is not None or Join._can_join(f, s):
|
|
172
|
+
idx.append(i)
|
|
173
|
+
break
|
|
174
|
+
elif onclause is None and isinstance(s, Lateral):
|
|
175
|
+
# in snowflake, onclause is not accepted for lateral due to BCR change:
|
|
176
|
+
# https://docs.snowflake.com/en/release-notes/bcr-bundles/2023_04/bcr-1057
|
|
177
|
+
# sqlalchemy only allows join with on condition.
|
|
178
|
+
# to adapt to snowflake syntax change,
|
|
179
|
+
# we make the change such that when oncaluse is None and the right part is
|
|
180
|
+
# Lateral, we append the index indicating Lateral clause can be joined from with without onclause.
|
|
181
|
+
idx.append(i)
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
if len(idx) > 1:
|
|
185
|
+
# this is the same "hide froms" logic from
|
|
186
|
+
# Selectable._get_display_froms
|
|
187
|
+
toremove = set(chain(*[_expand_cloned(f._hide_froms) for f in clauses]))
|
|
188
|
+
idx = [i for i in idx if clauses[i] not in toremove]
|
|
189
|
+
|
|
190
|
+
# onclause was given and none of them resolved, so assume
|
|
191
|
+
# all indexes can match
|
|
192
|
+
if not idx and onclause is not None:
|
|
193
|
+
return range(len(clauses))
|
|
194
|
+
else:
|
|
195
|
+
return idx
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _Snowflake_ORMJoin(sa_orm_util_ORMJoin):
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
left,
|
|
202
|
+
right,
|
|
203
|
+
onclause=None,
|
|
204
|
+
isouter=False,
|
|
205
|
+
full=False,
|
|
206
|
+
_left_memo=None,
|
|
207
|
+
_right_memo=None,
|
|
208
|
+
_extra_criteria=(),
|
|
209
|
+
):
|
|
210
|
+
left_info = inspection.inspect(left)
|
|
211
|
+
|
|
212
|
+
right_info = inspection.inspect(right)
|
|
213
|
+
adapt_to = right_info.selectable
|
|
214
|
+
|
|
215
|
+
# used by joined eager loader
|
|
216
|
+
self._left_memo = _left_memo
|
|
217
|
+
self._right_memo = _right_memo
|
|
218
|
+
|
|
219
|
+
# legacy, for string attr name ON clause. if that's removed
|
|
220
|
+
# then the "_joined_from_info" concept can go
|
|
221
|
+
left_orm_info = getattr(left, "_joined_from_info", left_info)
|
|
222
|
+
self._joined_from_info = right_info
|
|
223
|
+
if isinstance(onclause, compat.string_types):
|
|
224
|
+
onclause = getattr(left_orm_info.entity, onclause)
|
|
225
|
+
# ####
|
|
226
|
+
|
|
227
|
+
if isinstance(onclause, attributes.QueryableAttribute):
|
|
228
|
+
on_selectable = onclause.comparator._source_selectable()
|
|
229
|
+
prop = onclause.property
|
|
230
|
+
_extra_criteria += onclause._extra_criteria
|
|
231
|
+
elif isinstance(onclause, MapperProperty):
|
|
232
|
+
# used internally by joined eager loader...possibly not ideal
|
|
233
|
+
prop = onclause
|
|
234
|
+
on_selectable = prop.parent.selectable
|
|
235
|
+
else:
|
|
236
|
+
prop = None
|
|
237
|
+
|
|
238
|
+
if prop:
|
|
239
|
+
left_selectable = left_info.selectable
|
|
240
|
+
|
|
241
|
+
if sql_util.clause_is_present(on_selectable, left_selectable):
|
|
242
|
+
adapt_from = on_selectable
|
|
243
|
+
else:
|
|
244
|
+
adapt_from = left_selectable
|
|
245
|
+
|
|
246
|
+
(
|
|
247
|
+
pj,
|
|
248
|
+
sj,
|
|
249
|
+
source,
|
|
250
|
+
dest,
|
|
251
|
+
secondary,
|
|
252
|
+
target_adapter,
|
|
253
|
+
) = prop._create_joins(
|
|
254
|
+
source_selectable=adapt_from,
|
|
255
|
+
dest_selectable=adapt_to,
|
|
256
|
+
source_polymorphic=True,
|
|
257
|
+
of_type_entity=right_info,
|
|
258
|
+
alias_secondary=True,
|
|
259
|
+
extra_criteria=_extra_criteria,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if sj is not None:
|
|
263
|
+
if isouter:
|
|
264
|
+
# note this is an inner join from secondary->right
|
|
265
|
+
right = sql.join(secondary, right, sj)
|
|
266
|
+
onclause = pj
|
|
267
|
+
else:
|
|
268
|
+
left = sql.join(left, secondary, pj, isouter)
|
|
269
|
+
onclause = sj
|
|
270
|
+
else:
|
|
271
|
+
onclause = pj
|
|
272
|
+
|
|
273
|
+
self._target_adapter = target_adapter
|
|
274
|
+
|
|
275
|
+
# we don't use the normal coercions logic for _ORMJoin
|
|
276
|
+
# (probably should), so do some gymnastics to get the entity.
|
|
277
|
+
# logic here is for #8721, which was a major bug in 1.4
|
|
278
|
+
# for almost two years, not reported/fixed until 1.4.43 (!)
|
|
279
|
+
if left_info.is_selectable:
|
|
280
|
+
parententity = left_selectable._annotations.get("parententity", None)
|
|
281
|
+
elif left_info.is_mapper or left_info.is_aliased_class:
|
|
282
|
+
parententity = left_info
|
|
283
|
+
else:
|
|
284
|
+
parententity = None
|
|
285
|
+
|
|
286
|
+
if parententity is not None:
|
|
287
|
+
self._annotations = self._annotations.union(
|
|
288
|
+
{"parententity": parententity}
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
augment_onclause = onclause is None and _extra_criteria
|
|
292
|
+
# handle Snowflake BCR bcr-1057
|
|
293
|
+
_Snowflake_Selectable_Join.__init__(self, left, right, onclause, isouter, full)
|
|
294
|
+
|
|
295
|
+
if augment_onclause:
|
|
296
|
+
self.onclause &= sql.and_(*_extra_criteria)
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
not prop
|
|
300
|
+
and getattr(right_info, "mapper", None)
|
|
301
|
+
and right_info.mapper.single
|
|
302
|
+
):
|
|
303
|
+
# if single inheritance target and we are using a manual
|
|
304
|
+
# or implicit ON clause, augment it the same way we'd augment the
|
|
305
|
+
# WHERE.
|
|
306
|
+
single_crit = right_info.mapper._single_table_criterion
|
|
307
|
+
if single_crit is not None:
|
|
308
|
+
if right_info.is_aliased_class:
|
|
309
|
+
single_crit = right_info._adapter.traverse(single_crit)
|
|
310
|
+
self.onclause = self.onclause & single_crit
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class _Snowflake_Selectable_Join(Join):
|
|
314
|
+
def __init__(self, left, right, onclause=None, isouter=False, full=False):
|
|
315
|
+
"""Construct a new :class:`_expression.Join`.
|
|
316
|
+
|
|
317
|
+
The usual entrypoint here is the :func:`_expression.join`
|
|
318
|
+
function or the :meth:`_expression.FromClause.join` method of any
|
|
319
|
+
:class:`_expression.FromClause` object.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
self.left = coercions.expect(roles.FromClauseRole, left, deannotate=True)
|
|
323
|
+
self.right = coercions.expect(
|
|
324
|
+
roles.FromClauseRole, right, deannotate=True
|
|
325
|
+
).self_group()
|
|
326
|
+
|
|
327
|
+
if onclause is None:
|
|
328
|
+
try:
|
|
329
|
+
self.onclause = self._match_primaries(self.left, self.right)
|
|
330
|
+
except NoForeignKeysError:
|
|
331
|
+
# handle Snowflake BCR bcr-1057
|
|
332
|
+
if isinstance(self.right, Lateral):
|
|
333
|
+
self.onclause = None
|
|
334
|
+
else:
|
|
335
|
+
raise
|
|
336
|
+
else:
|
|
337
|
+
# note: taken from If91f61527236fd4d7ae3cad1f24c38be921c90ba
|
|
338
|
+
# not merged yet
|
|
339
|
+
self.onclause = coercions.expect(roles.OnClauseRole, onclause).self_group(
|
|
340
|
+
against=operators._asbool
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
self.isouter = isouter
|
|
344
|
+
self.full = full
|