snowflake-sqlalchemy 1.5.2__py3-none-any.whl

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