google-cloud-spanner 3.55.0__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.
- google/cloud/spanner.py +47 -0
- google/cloud/spanner_admin_database_v1/__init__.py +146 -0
- google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
- google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_database_v1/py.typed +2 -0
- google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
- google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
- google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
- google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
- google/cloud/spanner_admin_database_v1/types/common.py +180 -0
- google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
- google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
- google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
- google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_instance_v1/py.typed +2 -0
- google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
- google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
- google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
- google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
- google/cloud/spanner_dbapi/__init__.py +93 -0
- google/cloud/spanner_dbapi/_helpers.py +113 -0
- google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
- google/cloud/spanner_dbapi/checksum.py +80 -0
- google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
- google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
- google/cloud/spanner_dbapi/connection.py +818 -0
- google/cloud/spanner_dbapi/cursor.py +609 -0
- google/cloud/spanner_dbapi/exceptions.py +172 -0
- google/cloud/spanner_dbapi/parse_utils.py +392 -0
- google/cloud/spanner_dbapi/parsed_statement.py +63 -0
- google/cloud/spanner_dbapi/parser.py +258 -0
- google/cloud/spanner_dbapi/partition_helper.py +41 -0
- google/cloud/spanner_dbapi/transaction_helper.py +294 -0
- google/cloud/spanner_dbapi/types.py +106 -0
- google/cloud/spanner_dbapi/utils.py +147 -0
- google/cloud/spanner_dbapi/version.py +20 -0
- google/cloud/spanner_v1/__init__.py +154 -0
- google/cloud/spanner_v1/_helpers.py +751 -0
- google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
- google/cloud/spanner_v1/backup.py +397 -0
- google/cloud/spanner_v1/batch.py +433 -0
- google/cloud/spanner_v1/client.py +538 -0
- google/cloud/spanner_v1/data_types.py +350 -0
- google/cloud/spanner_v1/database.py +1968 -0
- google/cloud/spanner_v1/database_sessions_manager.py +249 -0
- google/cloud/spanner_v1/gapic_metadata.json +268 -0
- google/cloud/spanner_v1/gapic_version.py +16 -0
- google/cloud/spanner_v1/instance.py +735 -0
- google/cloud/spanner_v1/keyset.py +193 -0
- google/cloud/spanner_v1/merged_result_set.py +146 -0
- google/cloud/spanner_v1/metrics/constants.py +71 -0
- google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
- google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
- google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
- google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
- google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
- google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
- google/cloud/spanner_v1/param_types.py +110 -0
- google/cloud/spanner_v1/pool.py +813 -0
- google/cloud/spanner_v1/py.typed +2 -0
- google/cloud/spanner_v1/request_id_header.py +64 -0
- google/cloud/spanner_v1/services/__init__.py +15 -0
- google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
- google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
- google/cloud/spanner_v1/services/spanner/client.py +2624 -0
- google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
- google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
- google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
- google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
- google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
- google/cloud/spanner_v1/session.py +631 -0
- google/cloud/spanner_v1/session_options.py +133 -0
- google/cloud/spanner_v1/snapshot.py +1057 -0
- google/cloud/spanner_v1/streamed.py +402 -0
- google/cloud/spanner_v1/table.py +181 -0
- google/cloud/spanner_v1/testing/__init__.py +0 -0
- google/cloud/spanner_v1/testing/database_test.py +121 -0
- google/cloud/spanner_v1/testing/interceptors.py +118 -0
- google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
- google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
- google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
- google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
- google/cloud/spanner_v1/transaction.py +747 -0
- google/cloud/spanner_v1/types/__init__.py +118 -0
- google/cloud/spanner_v1/types/commit_response.py +94 -0
- google/cloud/spanner_v1/types/keys.py +248 -0
- google/cloud/spanner_v1/types/mutation.py +201 -0
- google/cloud/spanner_v1/types/query_plan.py +220 -0
- google/cloud/spanner_v1/types/result_set.py +379 -0
- google/cloud/spanner_v1/types/spanner.py +1815 -0
- google/cloud/spanner_v1/types/transaction.py +818 -0
- google/cloud/spanner_v1/types/type.py +288 -0
- google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
- google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
- google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
- google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
- google_cloud_spanner-3.55.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Grammar for parsing VALUES:
|
|
17
|
+
VALUES := `VALUES(` + ARGS + `)`
|
|
18
|
+
ARGS := [EXPR,]*EXPR
|
|
19
|
+
EXPR := TERMINAL / FUNC
|
|
20
|
+
TERMINAL := `%s`
|
|
21
|
+
FUNC := alphanum + `(` + ARGS + `)`
|
|
22
|
+
alphanum := (a-zA-Z_)[0-9a-ZA-Z_]*
|
|
23
|
+
|
|
24
|
+
thus given:
|
|
25
|
+
statement: 'VALUES (%s, %s), (%s, LOWER(UPPER(%s))) , (%s)'
|
|
26
|
+
It'll parse:
|
|
27
|
+
VALUES
|
|
28
|
+
|- ARGS
|
|
29
|
+
|- (TERMINAL, TERMINAL)
|
|
30
|
+
|- (TERMINAL, FUNC
|
|
31
|
+
|- FUNC
|
|
32
|
+
|- (TERMINAL)
|
|
33
|
+
|- (TERMINAL)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from .exceptions import ProgrammingError
|
|
37
|
+
|
|
38
|
+
ARGS = "ARGS"
|
|
39
|
+
FUNC = "FUNC"
|
|
40
|
+
VALUES = "VALUES"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class func(object):
|
|
44
|
+
def __init__(self, func_name, args):
|
|
45
|
+
self.name = func_name
|
|
46
|
+
self.args = args
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return "%s%s" % (self.name, self.args)
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return self.__str__()
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other):
|
|
55
|
+
if type(self) is not type(other):
|
|
56
|
+
return False
|
|
57
|
+
if self.name != other.name:
|
|
58
|
+
return False
|
|
59
|
+
if not isinstance(other.args, type(self.args)):
|
|
60
|
+
return False
|
|
61
|
+
if len(self.args) != len(other.args):
|
|
62
|
+
return False
|
|
63
|
+
return self.args == other.args
|
|
64
|
+
|
|
65
|
+
def __len__(self):
|
|
66
|
+
return len(self.args)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class terminal(str):
|
|
70
|
+
"""Represent the unit symbol that can be part of a SQL values clause."""
|
|
71
|
+
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class a_args(object):
|
|
76
|
+
"""Expression arguments.
|
|
77
|
+
|
|
78
|
+
:type argv: list
|
|
79
|
+
:param argv: A List of expression arguments.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, argv):
|
|
83
|
+
self.argv = argv
|
|
84
|
+
|
|
85
|
+
def __str__(self):
|
|
86
|
+
return "(" + ", ".join([str(arg) for arg in self.argv]) + ")"
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
return self.__str__()
|
|
90
|
+
|
|
91
|
+
def has_expr(self):
|
|
92
|
+
return any([token for token in self.argv if not isinstance(token, terminal)])
|
|
93
|
+
|
|
94
|
+
def __len__(self):
|
|
95
|
+
return len(self.argv)
|
|
96
|
+
|
|
97
|
+
def __eq__(self, other):
|
|
98
|
+
if type(self) is not type(other):
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if len(self) != len(other):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
for i, item in enumerate(self):
|
|
105
|
+
if item != other[i]:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def __getitem__(self, index):
|
|
111
|
+
return self.argv[index]
|
|
112
|
+
|
|
113
|
+
def homogenous(self):
|
|
114
|
+
"""Check arguments of the expression to be homogeneous.
|
|
115
|
+
|
|
116
|
+
:rtype: bool
|
|
117
|
+
:return: True if all the arguments of the expression are in pyformat
|
|
118
|
+
and each has the same length, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
if not self._is_equal_length():
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
for arg in self.argv:
|
|
124
|
+
if isinstance(arg, terminal):
|
|
125
|
+
continue
|
|
126
|
+
elif isinstance(arg, a_args):
|
|
127
|
+
if not arg.homogenous():
|
|
128
|
+
return False
|
|
129
|
+
else:
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def _is_equal_length(self):
|
|
134
|
+
"""Return False if all the arguments have the same length.
|
|
135
|
+
|
|
136
|
+
:rtype: bool
|
|
137
|
+
:return: False if the sequences of the arguments have the same length.
|
|
138
|
+
"""
|
|
139
|
+
if len(self) == 0:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
arg0_len = len(self.argv[0])
|
|
143
|
+
for arg in self.argv[1:]:
|
|
144
|
+
if len(arg) != arg0_len:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class values(a_args):
|
|
151
|
+
"""A wrapper for values.
|
|
152
|
+
|
|
153
|
+
:rtype: str
|
|
154
|
+
:returns: A string of the values expression in a tree view.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __str__(self):
|
|
158
|
+
return "VALUES%s" % super().__str__()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
pyfmt_str = terminal("%s")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def expect(word, token):
|
|
165
|
+
"""Parse the given expression recursively.
|
|
166
|
+
|
|
167
|
+
:type word: str
|
|
168
|
+
:param word: A string expression.
|
|
169
|
+
|
|
170
|
+
:type token: str
|
|
171
|
+
:param token: An expression token.
|
|
172
|
+
|
|
173
|
+
:rtype: `Tuple(str, Any)`
|
|
174
|
+
:returns: A tuple containing the rest of the expression string and the
|
|
175
|
+
parse tree for the part of the expression that has already been
|
|
176
|
+
parsed.
|
|
177
|
+
|
|
178
|
+
:raises :class:`ProgrammingError`: If there is a parsing error.
|
|
179
|
+
"""
|
|
180
|
+
word = word.strip()
|
|
181
|
+
if token == VALUES:
|
|
182
|
+
if not word.startswith("VALUES"):
|
|
183
|
+
raise ProgrammingError("VALUES: `%s` does not start with VALUES" % word)
|
|
184
|
+
|
|
185
|
+
word = word[len("VALUES") :].lstrip()
|
|
186
|
+
|
|
187
|
+
all_args = []
|
|
188
|
+
while word:
|
|
189
|
+
word = word.strip()
|
|
190
|
+
|
|
191
|
+
word, arg = expect(word, ARGS)
|
|
192
|
+
all_args.append(arg)
|
|
193
|
+
word = word.strip()
|
|
194
|
+
|
|
195
|
+
if word and not word.startswith(","):
|
|
196
|
+
raise ProgrammingError(
|
|
197
|
+
"VALUES: expected `,` got %s in %s" % (word[0], word)
|
|
198
|
+
)
|
|
199
|
+
word = word[1:]
|
|
200
|
+
return "", values(all_args)
|
|
201
|
+
|
|
202
|
+
elif token == FUNC:
|
|
203
|
+
begins_with_letter = word and (word[0].isalpha() or word[0] == "_")
|
|
204
|
+
if not begins_with_letter:
|
|
205
|
+
raise ProgrammingError(
|
|
206
|
+
"FUNC: `%s` does not begin with `a-zA-z` nor a `_`" % word
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
rest = word[1:]
|
|
210
|
+
end = 0
|
|
211
|
+
for ch in rest:
|
|
212
|
+
if ch.isalnum() or ch == "_":
|
|
213
|
+
end += 1
|
|
214
|
+
else:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
func_name, rest = word[: end + 1], word[end + 1 :].strip()
|
|
218
|
+
|
|
219
|
+
word, args = expect(rest, ARGS)
|
|
220
|
+
return word, func(func_name, args)
|
|
221
|
+
|
|
222
|
+
elif token == ARGS:
|
|
223
|
+
# The form should be:
|
|
224
|
+
# (%s)
|
|
225
|
+
# (%s, %s...)
|
|
226
|
+
# (FUNC, %s...)
|
|
227
|
+
# (%s, %s...)
|
|
228
|
+
if not (word and word.startswith("(")):
|
|
229
|
+
raise ProgrammingError("ARGS: supposed to begin with `(` in `%s`" % word)
|
|
230
|
+
|
|
231
|
+
word = word[1:]
|
|
232
|
+
|
|
233
|
+
terms = []
|
|
234
|
+
while True:
|
|
235
|
+
word = word.strip()
|
|
236
|
+
if not word or word.startswith(")"):
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
if word == "%s":
|
|
240
|
+
terms.append(pyfmt_str)
|
|
241
|
+
word = ""
|
|
242
|
+
elif not word.startswith("%s"):
|
|
243
|
+
word, parsed = expect(word, FUNC)
|
|
244
|
+
terms.append(parsed)
|
|
245
|
+
else:
|
|
246
|
+
terms.append(pyfmt_str)
|
|
247
|
+
word = word[2:].strip()
|
|
248
|
+
|
|
249
|
+
if word.startswith(","):
|
|
250
|
+
word = word[1:]
|
|
251
|
+
|
|
252
|
+
if not (word and word.startswith(")")):
|
|
253
|
+
raise ProgrammingError("ARGS: supposed to end with `)` in `%s`" % word)
|
|
254
|
+
|
|
255
|
+
word = word[1:]
|
|
256
|
+
return word, a_args(terms)
|
|
257
|
+
|
|
258
|
+
raise ProgrammingError("Unknown token `%s`" % token)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import gzip
|
|
19
|
+
import pickle
|
|
20
|
+
import base64
|
|
21
|
+
|
|
22
|
+
from google.cloud.spanner_v1 import BatchTransactionId
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode_from_string(encoded_partition_id):
|
|
26
|
+
gzip_bytes = base64.b64decode(bytes(encoded_partition_id, "utf-8"))
|
|
27
|
+
partition_id_bytes = gzip.decompress(gzip_bytes)
|
|
28
|
+
return pickle.loads(partition_id_bytes)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode_to_string(batch_transaction_id, partition_result):
|
|
32
|
+
partition_id = PartitionId(batch_transaction_id, partition_result)
|
|
33
|
+
partition_id_bytes = pickle.dumps(partition_id)
|
|
34
|
+
gzip_bytes = gzip.compress(partition_id_bytes)
|
|
35
|
+
return str(base64.b64encode(gzip_bytes), "utf-8")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PartitionId:
|
|
40
|
+
batch_transaction_id: BatchTransactionId
|
|
41
|
+
partition_result: Any
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import TYPE_CHECKING, List, Any, Dict
|
|
17
|
+
from google.api_core.exceptions import Aborted
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from google.cloud.spanner_dbapi.batch_dml_executor import BatchMode
|
|
22
|
+
from google.cloud.spanner_dbapi.exceptions import RetryAborted
|
|
23
|
+
from google.cloud.spanner_v1._helpers import _get_retry_delay
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from google.cloud.spanner_dbapi import Connection, Cursor
|
|
27
|
+
from google.cloud.spanner_dbapi.checksum import ResultsChecksum, _compare_checksums
|
|
28
|
+
|
|
29
|
+
MAX_INTERNAL_RETRIES = 50
|
|
30
|
+
RETRY_ABORTED_ERROR = "The transaction was aborted and could not be retried due to a concurrent modification."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TransactionRetryHelper:
|
|
34
|
+
def __init__(self, connection: "Connection"):
|
|
35
|
+
"""Helper class used in retrying the transaction when aborted This will
|
|
36
|
+
maintain all the statements executed on original transaction and replay
|
|
37
|
+
them again in the retried transaction.
|
|
38
|
+
|
|
39
|
+
:type connection: :class:`~google.cloud.spanner_dbapi.connection.Connection`
|
|
40
|
+
:param connection: A DB-API connection to Google Cloud Spanner.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self._connection = connection
|
|
44
|
+
# list of all statements in the same order as executed in original
|
|
45
|
+
# transaction along with their results
|
|
46
|
+
self._statement_result_details_list: List[StatementDetails] = []
|
|
47
|
+
# Map of last StatementDetails that was added to a particular cursor
|
|
48
|
+
self._last_statement_details_per_cursor: Dict[Cursor, StatementDetails] = {}
|
|
49
|
+
# 1-1 map from original cursor object on which transaction ran to the
|
|
50
|
+
# new cursor object used in the retry
|
|
51
|
+
self._cursor_map: Dict[Cursor, Cursor] = {}
|
|
52
|
+
|
|
53
|
+
def _set_connection_for_retry(self):
|
|
54
|
+
self._connection._spanner_transaction_started = False
|
|
55
|
+
self._connection._transaction_begin_marked = False
|
|
56
|
+
self._connection._batch_mode = BatchMode.NONE
|
|
57
|
+
|
|
58
|
+
def reset(self):
|
|
59
|
+
"""
|
|
60
|
+
Resets the state of the class when the ongoing transaction is committed
|
|
61
|
+
or aborted
|
|
62
|
+
"""
|
|
63
|
+
self._statement_result_details_list = []
|
|
64
|
+
self._last_statement_details_per_cursor = {}
|
|
65
|
+
self._cursor_map = {}
|
|
66
|
+
|
|
67
|
+
def add_fetch_statement_for_retry(
|
|
68
|
+
self, cursor, result_rows, exception, is_fetch_all
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
StatementDetails to be added to _statement_result_details_list whenever fetchone, fetchmany or
|
|
72
|
+
fetchall method is called on the cursor.
|
|
73
|
+
If fetchone is consecutively called n times then it is stored as fetchmany with size as n.
|
|
74
|
+
Same for fetchmany, so consecutive fetchone and fetchmany statements are stored as one
|
|
75
|
+
fetchmany statement in _statement_result_details_list with size param appropriately set
|
|
76
|
+
|
|
77
|
+
:param cursor: original Cursor object on which statement executed in the transaction
|
|
78
|
+
:param result_rows: All the rows from the resultSet from fetch statement execution
|
|
79
|
+
:param exception: Not none in case non-aborted exception is thrown on the original
|
|
80
|
+
statement execution
|
|
81
|
+
:param is_fetch_all: True in case of fetchall statement execution
|
|
82
|
+
"""
|
|
83
|
+
if not self._connection._client_transaction_started:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
last_statement_result_details = self._last_statement_details_per_cursor.get(
|
|
87
|
+
cursor
|
|
88
|
+
)
|
|
89
|
+
if (
|
|
90
|
+
last_statement_result_details is not None
|
|
91
|
+
and last_statement_result_details.statement_type
|
|
92
|
+
== CursorStatementType.FETCH_MANY
|
|
93
|
+
):
|
|
94
|
+
if exception is not None:
|
|
95
|
+
last_statement_result_details.result_type = ResultType.EXCEPTION
|
|
96
|
+
last_statement_result_details.result_details = exception
|
|
97
|
+
else:
|
|
98
|
+
for row in result_rows:
|
|
99
|
+
last_statement_result_details.result_details.consume_result(row)
|
|
100
|
+
last_statement_result_details.size += len(result_rows)
|
|
101
|
+
else:
|
|
102
|
+
result_details = _get_statement_result_checksum(result_rows)
|
|
103
|
+
if is_fetch_all:
|
|
104
|
+
statement_type = CursorStatementType.FETCH_ALL
|
|
105
|
+
size = None
|
|
106
|
+
else:
|
|
107
|
+
statement_type = CursorStatementType.FETCH_MANY
|
|
108
|
+
size = len(result_rows)
|
|
109
|
+
|
|
110
|
+
last_statement_result_details = FetchStatement(
|
|
111
|
+
cursor=cursor,
|
|
112
|
+
statement_type=statement_type,
|
|
113
|
+
result_type=ResultType.CHECKSUM,
|
|
114
|
+
result_details=result_details,
|
|
115
|
+
size=size,
|
|
116
|
+
)
|
|
117
|
+
self._last_statement_details_per_cursor[
|
|
118
|
+
cursor
|
|
119
|
+
] = last_statement_result_details
|
|
120
|
+
self._statement_result_details_list.append(last_statement_result_details)
|
|
121
|
+
|
|
122
|
+
def add_execute_statement_for_retry(
|
|
123
|
+
self, cursor, sql, args, exception, is_execute_many
|
|
124
|
+
):
|
|
125
|
+
"""
|
|
126
|
+
StatementDetails to be added to _statement_result_details_list whenever execute or
|
|
127
|
+
executemany method is called on the cursor.
|
|
128
|
+
|
|
129
|
+
:param cursor: original Cursor object on which statement executed in the transaction
|
|
130
|
+
:param sql: Input param of the execute/executemany method
|
|
131
|
+
:param args: Input param of the execute/executemany method
|
|
132
|
+
:param exception: Not none in case non-aborted exception is thrown on the original
|
|
133
|
+
statement execution
|
|
134
|
+
:param is_execute_many: True in case of executemany statement execution
|
|
135
|
+
"""
|
|
136
|
+
if not self._connection._client_transaction_started:
|
|
137
|
+
return
|
|
138
|
+
statement_type = CursorStatementType.EXECUTE
|
|
139
|
+
if is_execute_many:
|
|
140
|
+
statement_type = CursorStatementType.EXECUTE_MANY
|
|
141
|
+
|
|
142
|
+
result_type = ResultType.NONE
|
|
143
|
+
result_details = None
|
|
144
|
+
if exception is not None:
|
|
145
|
+
result_type = ResultType.EXCEPTION
|
|
146
|
+
result_details = exception
|
|
147
|
+
elif cursor._batch_dml_rows_count is not None:
|
|
148
|
+
result_type = ResultType.BATCH_DML_ROWS_COUNT
|
|
149
|
+
result_details = cursor._batch_dml_rows_count
|
|
150
|
+
elif cursor._row_count is not None:
|
|
151
|
+
result_type = ResultType.ROW_COUNT
|
|
152
|
+
result_details = cursor.rowcount
|
|
153
|
+
|
|
154
|
+
last_statement_result_details = ExecuteStatement(
|
|
155
|
+
cursor=cursor,
|
|
156
|
+
statement_type=statement_type,
|
|
157
|
+
sql=sql,
|
|
158
|
+
args=args,
|
|
159
|
+
result_type=result_type,
|
|
160
|
+
result_details=result_details,
|
|
161
|
+
)
|
|
162
|
+
self._last_statement_details_per_cursor[cursor] = last_statement_result_details
|
|
163
|
+
self._statement_result_details_list.append(last_statement_result_details)
|
|
164
|
+
|
|
165
|
+
def retry_transaction(self, default_retry_delay=None):
|
|
166
|
+
"""Retry the aborted transaction.
|
|
167
|
+
|
|
168
|
+
All the statements executed in the original transaction
|
|
169
|
+
will be re-executed in new one. Results checksums of the
|
|
170
|
+
original statements and the retried ones will be compared.
|
|
171
|
+
|
|
172
|
+
:raises: :class:`google.cloud.spanner_dbapi.exceptions.RetryAborted`
|
|
173
|
+
If results checksum of the retried statement is
|
|
174
|
+
not equal to the checksum of the original one.
|
|
175
|
+
"""
|
|
176
|
+
attempt = 0
|
|
177
|
+
while True:
|
|
178
|
+
attempt += 1
|
|
179
|
+
if attempt > MAX_INTERNAL_RETRIES:
|
|
180
|
+
raise
|
|
181
|
+
self._set_connection_for_retry()
|
|
182
|
+
try:
|
|
183
|
+
for statement_result_details in self._statement_result_details_list:
|
|
184
|
+
if statement_result_details.cursor in self._cursor_map:
|
|
185
|
+
cursor = self._cursor_map.get(statement_result_details.cursor)
|
|
186
|
+
else:
|
|
187
|
+
cursor = self._connection.cursor()
|
|
188
|
+
cursor._in_retry_mode = True
|
|
189
|
+
self._cursor_map[statement_result_details.cursor] = cursor
|
|
190
|
+
try:
|
|
191
|
+
_handle_statement(statement_result_details, cursor)
|
|
192
|
+
except Aborted:
|
|
193
|
+
raise
|
|
194
|
+
except RetryAborted:
|
|
195
|
+
raise
|
|
196
|
+
except Exception as ex:
|
|
197
|
+
if (
|
|
198
|
+
type(statement_result_details.result_details)
|
|
199
|
+
is not type(ex)
|
|
200
|
+
or ex.args != statement_result_details.result_details.args
|
|
201
|
+
):
|
|
202
|
+
raise RetryAborted(RETRY_ABORTED_ERROR, ex)
|
|
203
|
+
return
|
|
204
|
+
except Aborted as ex:
|
|
205
|
+
delay = _get_retry_delay(
|
|
206
|
+
ex.errors[0], attempt, default_retry_delay=default_retry_delay
|
|
207
|
+
)
|
|
208
|
+
if delay:
|
|
209
|
+
time.sleep(delay)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _handle_statement(statement_result_details, cursor):
|
|
213
|
+
statement_type = statement_result_details.statement_type
|
|
214
|
+
if _is_execute_type_statement(statement_type):
|
|
215
|
+
if statement_type == CursorStatementType.EXECUTE:
|
|
216
|
+
cursor.execute(statement_result_details.sql, statement_result_details.args)
|
|
217
|
+
if (
|
|
218
|
+
statement_result_details.result_type == ResultType.ROW_COUNT
|
|
219
|
+
and statement_result_details.result_details != cursor.rowcount
|
|
220
|
+
):
|
|
221
|
+
raise RetryAborted(RETRY_ABORTED_ERROR)
|
|
222
|
+
else:
|
|
223
|
+
cursor.executemany(
|
|
224
|
+
statement_result_details.sql, statement_result_details.args
|
|
225
|
+
)
|
|
226
|
+
if (
|
|
227
|
+
statement_result_details.result_type == ResultType.BATCH_DML_ROWS_COUNT
|
|
228
|
+
and statement_result_details.result_details != cursor._batch_dml_rows_count
|
|
229
|
+
):
|
|
230
|
+
raise RetryAborted(RETRY_ABORTED_ERROR)
|
|
231
|
+
else:
|
|
232
|
+
if statement_type == CursorStatementType.FETCH_ALL:
|
|
233
|
+
res = cursor.fetchall()
|
|
234
|
+
else:
|
|
235
|
+
res = cursor.fetchmany(statement_result_details.size)
|
|
236
|
+
checksum = _get_statement_result_checksum(res)
|
|
237
|
+
_compare_checksums(checksum, statement_result_details.result_details)
|
|
238
|
+
if statement_result_details.result_type == ResultType.EXCEPTION:
|
|
239
|
+
raise RetryAborted(RETRY_ABORTED_ERROR)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_execute_type_statement(statement_type):
|
|
243
|
+
return statement_type in (
|
|
244
|
+
CursorStatementType.EXECUTE,
|
|
245
|
+
CursorStatementType.EXECUTE_MANY,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_statement_result_checksum(res_iter):
|
|
250
|
+
retried_checksum = ResultsChecksum()
|
|
251
|
+
for res in res_iter:
|
|
252
|
+
retried_checksum.consume_result(res)
|
|
253
|
+
return retried_checksum
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class CursorStatementType(Enum):
|
|
257
|
+
EXECUTE = 1
|
|
258
|
+
EXECUTE_MANY = 2
|
|
259
|
+
FETCH_ONE = 3
|
|
260
|
+
FETCH_ALL = 4
|
|
261
|
+
FETCH_MANY = 5
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ResultType(Enum):
|
|
265
|
+
# checksum of ResultSet in case of fetch call on query statement
|
|
266
|
+
CHECKSUM = 1
|
|
267
|
+
# None in case of execute call on query statement
|
|
268
|
+
NONE = 2
|
|
269
|
+
# Exception details in case of any statement execution throws exception
|
|
270
|
+
EXCEPTION = 3
|
|
271
|
+
# Total rows updated in case of execute call on DML statement
|
|
272
|
+
ROW_COUNT = 4
|
|
273
|
+
# Total rows updated in case of Batch DML statement execution
|
|
274
|
+
BATCH_DML_ROWS_COUNT = 5
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class StatementDetails:
|
|
279
|
+
statement_type: CursorStatementType
|
|
280
|
+
# The cursor object on which this statement was executed
|
|
281
|
+
cursor: "Cursor"
|
|
282
|
+
result_type: ResultType
|
|
283
|
+
result_details: Any
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class ExecuteStatement(StatementDetails):
|
|
288
|
+
sql: str
|
|
289
|
+
args: Any = None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class FetchStatement(StatementDetails):
|
|
294
|
+
size: int = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Implementation of the type objects and constructors according to the
|
|
16
|
+
PEP-0249 specification.
|
|
17
|
+
|
|
18
|
+
See
|
|
19
|
+
https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import datetime
|
|
23
|
+
import time
|
|
24
|
+
from base64 import b64encode
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _date_from_ticks(ticks):
|
|
28
|
+
"""Based on PEP-249 Implementation Hints for Module Authors:
|
|
29
|
+
|
|
30
|
+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
|
|
31
|
+
"""
|
|
32
|
+
return Date(*time.localtime(ticks)[:3])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _time_from_ticks(ticks):
|
|
36
|
+
"""Based on PEP-249 Implementation Hints for Module Authors:
|
|
37
|
+
|
|
38
|
+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
|
|
39
|
+
"""
|
|
40
|
+
return Time(*time.localtime(ticks)[3:6])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _timestamp_from_ticks(ticks):
|
|
44
|
+
"""Based on PEP-249 Implementation Hints for Module Authors:
|
|
45
|
+
|
|
46
|
+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
|
|
47
|
+
"""
|
|
48
|
+
return Timestamp(*time.localtime(ticks)[:6])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _DBAPITypeObject(object):
|
|
52
|
+
"""Implementation of a helper class used for type comparison among similar
|
|
53
|
+
but possibly different types.
|
|
54
|
+
|
|
55
|
+
See
|
|
56
|
+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, *values):
|
|
60
|
+
self.values = values
|
|
61
|
+
|
|
62
|
+
def __eq__(self, other):
|
|
63
|
+
return other in self.values
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Date = datetime.date
|
|
67
|
+
Time = datetime.time
|
|
68
|
+
Timestamp = datetime.datetime
|
|
69
|
+
DateFromTicks = _date_from_ticks
|
|
70
|
+
TimeFromTicks = _time_from_ticks
|
|
71
|
+
TimestampFromTicks = _timestamp_from_ticks
|
|
72
|
+
Binary = b64encode
|
|
73
|
+
|
|
74
|
+
STRING = "STRING"
|
|
75
|
+
BINARY = _DBAPITypeObject("TYPE_CODE_UNSPECIFIED", "BYTES", "ARRAY", "STRUCT")
|
|
76
|
+
NUMBER = _DBAPITypeObject("BOOL", "INT64", "FLOAT64", "FLOAT32", "NUMERIC")
|
|
77
|
+
DATETIME = _DBAPITypeObject("TIMESTAMP", "DATE")
|
|
78
|
+
ROWID = "STRING"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TimestampStr(str):
|
|
82
|
+
"""[inherited from the alpha release]
|
|
83
|
+
|
|
84
|
+
TODO: Decide whether this class is necessary
|
|
85
|
+
|
|
86
|
+
TimestampStr exists so that we can purposefully format types as timestamps
|
|
87
|
+
compatible with Cloud Spanner's TIMESTAMP type, but right before making
|
|
88
|
+
queries, it'll help differentiate between normal strings and the case of
|
|
89
|
+
types that should be TIMESTAMP.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DateStr(str):
|
|
96
|
+
"""[inherited from the alpha release]
|
|
97
|
+
|
|
98
|
+
TODO: Decide whether this class is necessary
|
|
99
|
+
|
|
100
|
+
DateStr is a sentinel type to help format Django dates as
|
|
101
|
+
compatible with Cloud Spanner's DATE type, but right before making
|
|
102
|
+
queries, it'll help differentiate between normal strings and the case of
|
|
103
|
+
types that should be DATE.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
pass
|