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.
Files changed (119) hide show
  1. google/cloud/spanner.py +47 -0
  2. google/cloud/spanner_admin_database_v1/__init__.py +146 -0
  3. google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
  4. google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
  5. google/cloud/spanner_admin_database_v1/py.typed +2 -0
  6. google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
  7. google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
  8. google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
  9. google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
  10. google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
  11. google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
  12. google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
  13. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
  14. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
  15. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
  16. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
  17. google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
  18. google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
  19. google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
  20. google/cloud/spanner_admin_database_v1/types/common.py +180 -0
  21. google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
  22. google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
  23. google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
  24. google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
  25. google/cloud/spanner_admin_instance_v1/py.typed +2 -0
  26. google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
  27. google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
  28. google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
  29. google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
  30. google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
  31. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
  32. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
  33. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
  34. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
  35. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
  36. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
  37. google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
  38. google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
  39. google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
  40. google/cloud/spanner_dbapi/__init__.py +93 -0
  41. google/cloud/spanner_dbapi/_helpers.py +113 -0
  42. google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
  43. google/cloud/spanner_dbapi/checksum.py +80 -0
  44. google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
  45. google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
  46. google/cloud/spanner_dbapi/connection.py +818 -0
  47. google/cloud/spanner_dbapi/cursor.py +609 -0
  48. google/cloud/spanner_dbapi/exceptions.py +172 -0
  49. google/cloud/spanner_dbapi/parse_utils.py +392 -0
  50. google/cloud/spanner_dbapi/parsed_statement.py +63 -0
  51. google/cloud/spanner_dbapi/parser.py +258 -0
  52. google/cloud/spanner_dbapi/partition_helper.py +41 -0
  53. google/cloud/spanner_dbapi/transaction_helper.py +294 -0
  54. google/cloud/spanner_dbapi/types.py +106 -0
  55. google/cloud/spanner_dbapi/utils.py +147 -0
  56. google/cloud/spanner_dbapi/version.py +20 -0
  57. google/cloud/spanner_v1/__init__.py +154 -0
  58. google/cloud/spanner_v1/_helpers.py +751 -0
  59. google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
  60. google/cloud/spanner_v1/backup.py +397 -0
  61. google/cloud/spanner_v1/batch.py +433 -0
  62. google/cloud/spanner_v1/client.py +538 -0
  63. google/cloud/spanner_v1/data_types.py +350 -0
  64. google/cloud/spanner_v1/database.py +1968 -0
  65. google/cloud/spanner_v1/database_sessions_manager.py +249 -0
  66. google/cloud/spanner_v1/gapic_metadata.json +268 -0
  67. google/cloud/spanner_v1/gapic_version.py +16 -0
  68. google/cloud/spanner_v1/instance.py +735 -0
  69. google/cloud/spanner_v1/keyset.py +193 -0
  70. google/cloud/spanner_v1/merged_result_set.py +146 -0
  71. google/cloud/spanner_v1/metrics/constants.py +71 -0
  72. google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
  73. google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
  74. google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
  75. google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
  76. google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
  77. google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
  78. google/cloud/spanner_v1/param_types.py +110 -0
  79. google/cloud/spanner_v1/pool.py +813 -0
  80. google/cloud/spanner_v1/py.typed +2 -0
  81. google/cloud/spanner_v1/request_id_header.py +64 -0
  82. google/cloud/spanner_v1/services/__init__.py +15 -0
  83. google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
  84. google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
  85. google/cloud/spanner_v1/services/spanner/client.py +2624 -0
  86. google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
  87. google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
  88. google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
  89. google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
  90. google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
  91. google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
  92. google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
  93. google/cloud/spanner_v1/session.py +631 -0
  94. google/cloud/spanner_v1/session_options.py +133 -0
  95. google/cloud/spanner_v1/snapshot.py +1057 -0
  96. google/cloud/spanner_v1/streamed.py +402 -0
  97. google/cloud/spanner_v1/table.py +181 -0
  98. google/cloud/spanner_v1/testing/__init__.py +0 -0
  99. google/cloud/spanner_v1/testing/database_test.py +121 -0
  100. google/cloud/spanner_v1/testing/interceptors.py +118 -0
  101. google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
  102. google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
  103. google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
  104. google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
  105. google/cloud/spanner_v1/transaction.py +747 -0
  106. google/cloud/spanner_v1/types/__init__.py +118 -0
  107. google/cloud/spanner_v1/types/commit_response.py +94 -0
  108. google/cloud/spanner_v1/types/keys.py +248 -0
  109. google/cloud/spanner_v1/types/mutation.py +201 -0
  110. google/cloud/spanner_v1/types/query_plan.py +220 -0
  111. google/cloud/spanner_v1/types/result_set.py +379 -0
  112. google/cloud/spanner_v1/types/spanner.py +1815 -0
  113. google/cloud/spanner_v1/types/transaction.py +818 -0
  114. google/cloud/spanner_v1/types/type.py +288 -0
  115. google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
  116. google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
  117. google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
  118. google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
  119. 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