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,402 @@
1
+ # Copyright 2016 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
+ """Wrapper for streaming results."""
16
+
17
+ from google.cloud import exceptions
18
+ from google.protobuf.struct_pb2 import ListValue
19
+ from google.protobuf.struct_pb2 import Value
20
+
21
+ from google.cloud.spanner_v1 import PartialResultSet
22
+ from google.cloud.spanner_v1 import ResultSetMetadata
23
+ from google.cloud.spanner_v1 import TypeCode
24
+ from google.cloud.spanner_v1._helpers import _get_type_decoder, _parse_nullable
25
+
26
+
27
+ class StreamedResultSet(object):
28
+ """Process a sequence of partial result sets into a single set of row data.
29
+
30
+ :type response_iterator:
31
+ :param response_iterator:
32
+ Iterator yielding
33
+ :class:`~google.cloud.spanner_v1.types.PartialResultSet`
34
+ instances.
35
+
36
+ :type source: :class:`~google.cloud.spanner_v1.snapshot.Snapshot`
37
+ :param source: Snapshot from which the result set was fetched.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ response_iterator,
43
+ source=None,
44
+ column_info=None,
45
+ lazy_decode: bool = False,
46
+ ):
47
+ self._response_iterator = response_iterator
48
+ self._rows = [] # Fully-processed rows
49
+ self._metadata = None # Until set from first PRS
50
+ self._stats = None # Until set from last PRS
51
+ self._current_row = [] # Accumulated values for incomplete row
52
+ self._pending_chunk = None # Incomplete value
53
+ self._source = source # Source snapshot
54
+ self._column_info = column_info # Column information
55
+ self._field_decoders = None
56
+ self._lazy_decode = lazy_decode # Return protobuf values
57
+
58
+ @property
59
+ def fields(self):
60
+ """Field descriptors for result set columns.
61
+
62
+ :rtype: list of :class:`~google.cloud.spanner_v1.types.StructType.Field`
63
+ :returns: list of fields describing column names / types.
64
+ """
65
+ return self._metadata.row_type.fields
66
+
67
+ @property
68
+ def metadata(self):
69
+ """Result set metadata
70
+
71
+ :rtype: :class:`~google.cloud.spanner_v1.types.ResultSetMetadata`
72
+ :returns: structure describing the results
73
+ """
74
+ if self._metadata:
75
+ return ResultSetMetadata.wrap(self._metadata)
76
+ return None
77
+
78
+ @property
79
+ def stats(self):
80
+ """Result set statistics
81
+
82
+ :rtype:
83
+ :class:`~google.cloud.spanner_v1.types.ResultSetStats`
84
+ :returns: structure describing status about the response
85
+ """
86
+ return self._stats
87
+
88
+ @property
89
+ def _decoders(self):
90
+ if self._field_decoders is None:
91
+ if self._metadata is None:
92
+ raise ValueError("iterator not started")
93
+ self._field_decoders = [
94
+ _get_type_decoder(field.type_, field.name, self._column_info)
95
+ for field in self.fields
96
+ ]
97
+ return self._field_decoders
98
+
99
+ def _merge_chunk(self, value):
100
+ """Merge pending chunk with next value.
101
+
102
+ :type value: :class:`~google.protobuf.struct_pb2.Value`
103
+ :param value: continuation of chunked value from previous
104
+ partial result set.
105
+
106
+ :rtype: :class:`~google.protobuf.struct_pb2.Value`
107
+ :returns: the merged value
108
+ """
109
+ current_column = len(self._current_row)
110
+ field = self.fields[current_column]
111
+ merged = _merge_by_type(self._pending_chunk, value, field.type_)
112
+ self._pending_chunk = None
113
+ return merged
114
+
115
+ def _merge_values(self, values):
116
+ """Merge values into rows.
117
+
118
+ :type values: list of :class:`~google.protobuf.struct_pb2.Value`
119
+ :param values: non-chunked values from partial result set.
120
+ """
121
+ decoders = self._decoders
122
+ width = len(self.fields)
123
+ index = len(self._current_row)
124
+ for value in values:
125
+ if self._lazy_decode:
126
+ self._current_row.append(value)
127
+ else:
128
+ self._current_row.append(_parse_nullable(value, decoders[index]))
129
+ index += 1
130
+ if index == width:
131
+ self._rows.append(self._current_row)
132
+ self._current_row = []
133
+ index = 0
134
+
135
+ def _consume_next(self):
136
+ """Consume the next partial result set from the stream.
137
+
138
+ Parse the result set into new/existing rows in :attr:`_rows`
139
+ """
140
+ response = next(self._response_iterator)
141
+ response_pb = PartialResultSet.pb(response)
142
+
143
+ if self._metadata is None: # first response
144
+ metadata = self._metadata = response_pb.metadata
145
+
146
+ source = self._source
147
+ if source is not None and source._transaction_id is None:
148
+ source._transaction_id = metadata.transaction.id
149
+
150
+ if response_pb.HasField("stats"): # last response
151
+ self._stats = response.stats
152
+
153
+ values = list(response_pb.values)
154
+ if self._pending_chunk is not None:
155
+ values[0] = self._merge_chunk(values[0])
156
+
157
+ if response_pb.chunked_value:
158
+ self._pending_chunk = values.pop()
159
+
160
+ self._merge_values(values)
161
+
162
+ def __iter__(self):
163
+ while True:
164
+ iter_rows, self._rows[:] = self._rows[:], ()
165
+ while iter_rows:
166
+ yield iter_rows.pop(0)
167
+ try:
168
+ self._consume_next()
169
+ except StopIteration:
170
+ return
171
+
172
+ def decode_row(self, row: []) -> []:
173
+ """Decodes a row from protobuf values to Python objects. This function
174
+ should only be called for result sets that use ``lazy_decoding=True``.
175
+ The array that is returned by this function is the same as the array
176
+ that would have been returned by the rows iterator if ``lazy_decoding=False``.
177
+
178
+ :returns: an array containing the decoded values of all the columns in the given row
179
+ """
180
+ if not hasattr(row, "__len__"):
181
+ raise TypeError("row", "row must be an array of protobuf values")
182
+ decoders = self._decoders
183
+ return [
184
+ _parse_nullable(row[index], decoders[index]) for index in range(len(row))
185
+ ]
186
+
187
+ def decode_column(self, row: [], column_index: int):
188
+ """Decodes a column from a protobuf value to a Python object. This function
189
+ should only be called for result sets that use ``lazy_decoding=True``.
190
+ The object that is returned by this function is the same as the object
191
+ that would have been returned by the rows iterator if ``lazy_decoding=False``.
192
+
193
+ :returns: the decoded column value
194
+ """
195
+ if not hasattr(row, "__len__"):
196
+ raise TypeError("row", "row must be an array of protobuf values")
197
+ decoders = self._decoders
198
+ return _parse_nullable(row[column_index], decoders[column_index])
199
+
200
+ def one(self):
201
+ """Return exactly one result, or raise an exception.
202
+
203
+ :raises: :exc:`NotFound`: If there are no results.
204
+ :raises: :exc:`ValueError`: If there are multiple results.
205
+ :raises: :exc:`RuntimeError`: If consumption has already occurred,
206
+ in whole or in part.
207
+ """
208
+ answer = self.one_or_none()
209
+ if answer is None:
210
+ raise exceptions.NotFound("No rows matched the given query.")
211
+ return answer
212
+
213
+ def one_or_none(self):
214
+ """Return exactly one result, or None if there are no results.
215
+
216
+ :raises: :exc:`ValueError`: If there are multiple results.
217
+ :raises: :exc:`RuntimeError`: If consumption has already occurred,
218
+ in whole or in part.
219
+ """
220
+ # Sanity check: Has consumption of this query already started?
221
+ # If it has, then this is an exception.
222
+ if self._metadata is not None:
223
+ raise RuntimeError(
224
+ "Can not call `.one` or `.one_or_none` after "
225
+ "stream consumption has already started."
226
+ )
227
+
228
+ # Consume the first result of the stream.
229
+ # If there is no first result, then return None.
230
+ iterator = iter(self)
231
+ try:
232
+ answer = next(iterator)
233
+ except StopIteration:
234
+ return None
235
+
236
+ # Attempt to consume more. This should no-op; if we get additional
237
+ # rows, then this is an error case.
238
+ try:
239
+ next(iterator)
240
+ raise ValueError("Expected one result; got more.")
241
+ except StopIteration:
242
+ return answer
243
+
244
+ def to_dict_list(self):
245
+ """Return the result of a query as a list of dictionaries.
246
+ In each dictionary the key is the column name and the value is the
247
+ value of the that column in a given row.
248
+
249
+ :rtype:
250
+ :class:`list of dict`
251
+ :returns: result rows as a list of dictionaries
252
+ """
253
+ rows = []
254
+ for row in self:
255
+ rows.append(
256
+ {
257
+ column: value
258
+ for column, value in zip(
259
+ [column.name for column in self._metadata.row_type.fields], row
260
+ )
261
+ }
262
+ )
263
+ return rows
264
+
265
+
266
+ class Unmergeable(ValueError):
267
+ """Unable to merge two values.
268
+
269
+ :type lhs: :class:`~google.protobuf.struct_pb2.Value`
270
+ :param lhs: pending value to be merged
271
+
272
+ :type rhs: :class:`~google.protobuf.struct_pb2.Value`
273
+ :param rhs: remaining value to be merged
274
+
275
+ :type type_: :class:`~google.cloud.spanner_v1.types.Type`
276
+ :param type_: field type of values being merged
277
+ """
278
+
279
+ def __init__(self, lhs, rhs, type_):
280
+ message = "Cannot merge %s values: %s %s" % (
281
+ TypeCode(type_.code),
282
+ lhs,
283
+ rhs,
284
+ )
285
+ super(Unmergeable, self).__init__(message)
286
+
287
+
288
+ def _unmergeable(lhs, rhs, type_):
289
+ """Helper for '_merge_by_type'."""
290
+ raise Unmergeable(lhs, rhs, type_)
291
+
292
+
293
+ def _merge_float64(lhs, rhs, type_):
294
+ """Helper for '_merge_by_type'."""
295
+ lhs_kind = lhs.WhichOneof("kind")
296
+ if lhs_kind == "string_value":
297
+ return Value(string_value=lhs.string_value + rhs.string_value)
298
+ rhs_kind = rhs.WhichOneof("kind")
299
+ array_continuation = (
300
+ lhs_kind == "number_value"
301
+ and rhs_kind == "string_value"
302
+ and rhs.string_value == ""
303
+ )
304
+ if array_continuation:
305
+ return lhs
306
+ raise Unmergeable(lhs, rhs, type_)
307
+
308
+
309
+ def _merge_string(lhs, rhs, type_):
310
+ """Helper for '_merge_by_type'."""
311
+ return Value(string_value=lhs.string_value + rhs.string_value)
312
+
313
+
314
+ _UNMERGEABLE_TYPES = (TypeCode.BOOL,)
315
+
316
+
317
+ def _merge_array(lhs, rhs, type_):
318
+ """Helper for '_merge_by_type'."""
319
+ element_type = type_.array_element_type
320
+ if element_type.code in _UNMERGEABLE_TYPES:
321
+ # Individual values cannot be merged, just concatenate
322
+ lhs.list_value.values.extend(rhs.list_value.values)
323
+ return lhs
324
+ lhs, rhs = list(lhs.list_value.values), list(rhs.list_value.values)
325
+
326
+ # Sanity check: If either list is empty, short-circuit.
327
+ # This is effectively a no-op.
328
+ if not len(lhs) or not len(rhs):
329
+ return Value(list_value=ListValue(values=(lhs + rhs)))
330
+
331
+ first = rhs.pop(0)
332
+ if first.HasField("null_value"): # can't merge
333
+ lhs.append(first)
334
+ else:
335
+ last = lhs.pop()
336
+ if last.HasField("null_value"):
337
+ lhs.append(last)
338
+ lhs.append(first)
339
+ else:
340
+ try:
341
+ merged = _merge_by_type(last, first, element_type)
342
+ except Unmergeable:
343
+ lhs.append(last)
344
+ lhs.append(first)
345
+ else:
346
+ lhs.append(merged)
347
+ return Value(list_value=ListValue(values=(lhs + rhs)))
348
+
349
+
350
+ def _merge_struct(lhs, rhs, type_):
351
+ """Helper for '_merge_by_type'."""
352
+ fields = type_.struct_type.fields
353
+ lhs, rhs = list(lhs.list_value.values), list(rhs.list_value.values)
354
+
355
+ # Sanity check: If either list is empty, short-circuit.
356
+ # This is effectively a no-op.
357
+ if not len(lhs) or not len(rhs):
358
+ return Value(list_value=ListValue(values=(lhs + rhs)))
359
+
360
+ candidate_type = fields[len(lhs) - 1].type_
361
+ first = rhs.pop(0)
362
+ if first.HasField("null_value") or candidate_type.code in _UNMERGEABLE_TYPES:
363
+ lhs.append(first)
364
+ else:
365
+ last = lhs.pop()
366
+ if last.HasField("null_value"):
367
+ lhs.append(last)
368
+ lhs.append(first)
369
+ else:
370
+ try:
371
+ merged = _merge_by_type(last, first, candidate_type)
372
+ except Unmergeable:
373
+ lhs.append(last)
374
+ lhs.append(first)
375
+ else:
376
+ lhs.append(merged)
377
+ return Value(list_value=ListValue(values=lhs + rhs))
378
+
379
+
380
+ _MERGE_BY_TYPE = {
381
+ TypeCode.ARRAY: _merge_array,
382
+ TypeCode.BOOL: _unmergeable,
383
+ TypeCode.BYTES: _merge_string,
384
+ TypeCode.DATE: _merge_string,
385
+ TypeCode.FLOAT64: _merge_float64,
386
+ TypeCode.FLOAT32: _merge_float64,
387
+ TypeCode.INT64: _merge_string,
388
+ TypeCode.STRING: _merge_string,
389
+ TypeCode.STRUCT: _merge_struct,
390
+ TypeCode.TIMESTAMP: _merge_string,
391
+ TypeCode.NUMERIC: _merge_string,
392
+ TypeCode.JSON: _merge_string,
393
+ TypeCode.PROTO: _merge_string,
394
+ TypeCode.INTERVAL: _merge_string,
395
+ TypeCode.ENUM: _merge_string,
396
+ }
397
+
398
+
399
+ def _merge_by_type(lhs, rhs, type_):
400
+ """Helper for '_merge_chunk'."""
401
+ merger = _MERGE_BY_TYPE[type_.code]
402
+ return merger(lhs, rhs, type_)
@@ -0,0 +1,181 @@
1
+ # Copyright 2021 Google LLC
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
+ """User friendly container for Cloud Spanner Table."""
16
+
17
+ from google.cloud.exceptions import NotFound
18
+
19
+ from google.cloud.spanner_admin_database_v1 import DatabaseDialect
20
+ from google.cloud.spanner_v1.types import (
21
+ Type,
22
+ TypeCode,
23
+ )
24
+
25
+
26
+ _EXISTS_TEMPLATE = """
27
+ SELECT EXISTS(
28
+ SELECT TABLE_NAME
29
+ FROM INFORMATION_SCHEMA.TABLES
30
+ {}
31
+ )
32
+ """
33
+ _GET_SCHEMA_TEMPLATE = "SELECT * FROM {} LIMIT 0"
34
+
35
+
36
+ class Table(object):
37
+ """Representation of a Cloud Spanner Table.
38
+
39
+ :type table_id: str
40
+ :param table_id: The ID of the table.
41
+
42
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
43
+ :param database: The database that owns the table.
44
+ """
45
+
46
+ def __init__(self, table_id, database, schema_name=None):
47
+ if schema_name is None:
48
+ self._schema_name = database.default_schema_name
49
+ else:
50
+ self._schema_name = schema_name
51
+ self._table_id = table_id
52
+ self._database = database
53
+
54
+ # Calculated properties.
55
+ self._schema = None
56
+
57
+ @property
58
+ def schema_name(self):
59
+ """The schema name of the table used in SQL.
60
+
61
+ :rtype: str
62
+ :returns: The table schema name.
63
+ """
64
+ return self._schema_name
65
+
66
+ @property
67
+ def table_id(self):
68
+ """The ID of the table used in SQL.
69
+
70
+ :rtype: str
71
+ :returns: The table ID.
72
+ """
73
+ return self._table_id
74
+
75
+ @property
76
+ def qualified_table_name(self):
77
+ """The qualified name of the table used in SQL.
78
+
79
+ :rtype: str
80
+ :returns: The qualified table name.
81
+ """
82
+ if self.schema_name == self._database.default_schema_name:
83
+ return self._quote_identifier(self.table_id)
84
+ return "{}.{}".format(
85
+ self._quote_identifier(self.schema_name),
86
+ self._quote_identifier(self.table_id),
87
+ )
88
+
89
+ def _quote_identifier(self, identifier):
90
+ """Quotes the given identifier using the rules of the dialect of the database of this table.
91
+
92
+ :rtype: str
93
+ :returns: The quoted identifier.
94
+ """
95
+ if self._database.database_dialect == DatabaseDialect.POSTGRESQL:
96
+ return '"{}"'.format(identifier)
97
+ return "`{}`".format(identifier)
98
+
99
+ def exists(self):
100
+ """Test whether this table exists.
101
+
102
+ :rtype: bool
103
+ :returns: True if the table exists, else false.
104
+ """
105
+ with self._database.snapshot() as snapshot:
106
+ return self._exists(snapshot)
107
+
108
+ def _exists(self, snapshot):
109
+ """Query to check that the table exists.
110
+
111
+ :type snapshot: :class:`~google.cloud.spanner_v1.snapshot.Snapshot`
112
+ :param snapshot: snapshot to use for database queries
113
+
114
+ :rtype: bool
115
+ :returns: True if the table exists, else false.
116
+ """
117
+ if self._database.database_dialect == DatabaseDialect.POSTGRESQL:
118
+ results = snapshot.execute_sql(
119
+ sql=_EXISTS_TEMPLATE.format(
120
+ "WHERE TABLE_SCHEMA=$1 AND TABLE_NAME = $2"
121
+ ),
122
+ params={"p1": self.schema_name, "p2": self.table_id},
123
+ param_types={
124
+ "p1": Type(code=TypeCode.STRING),
125
+ "p2": Type(code=TypeCode.STRING),
126
+ },
127
+ )
128
+ else:
129
+ results = snapshot.execute_sql(
130
+ sql=_EXISTS_TEMPLATE.format(
131
+ "WHERE TABLE_SCHEMA = @schema_name AND TABLE_NAME = @table_id"
132
+ ),
133
+ params={"schema_name": self.schema_name, "table_id": self.table_id},
134
+ param_types={
135
+ "schema_name": Type(code=TypeCode.STRING),
136
+ "table_id": Type(code=TypeCode.STRING),
137
+ },
138
+ )
139
+ return next(iter(results))[0]
140
+
141
+ @property
142
+ def schema(self):
143
+ """The schema of this table.
144
+
145
+ :rtype: list of :class:`~google.cloud.spanner_v1.types.StructType.Field`
146
+ :returns: The table schema.
147
+ """
148
+ if self._schema is None:
149
+ with self._database.snapshot() as snapshot:
150
+ self._schema = self._get_schema(snapshot)
151
+ return self._schema
152
+
153
+ def _get_schema(self, snapshot):
154
+ """Get the schema of this table.
155
+
156
+ :type snapshot: :class:`~google.cloud.spanner_v1.snapshot.Snapshot`
157
+ :param snapshot: snapshot to use for database queries
158
+
159
+ :rtype: list of :class:`~google.cloud.spanner_v1.types.StructType.Field`
160
+ :returns: The table schema.
161
+ """
162
+ query = _GET_SCHEMA_TEMPLATE.format(self.qualified_table_name)
163
+ results = snapshot.execute_sql(query)
164
+ # Start iterating to force the schema to download.
165
+ try:
166
+ next(iter(results))
167
+ except StopIteration:
168
+ pass
169
+ return list(results.fields)
170
+
171
+ def reload(self):
172
+ """Reload this table.
173
+
174
+ Refresh any configured schema into :attr:`schema`.
175
+
176
+ :raises NotFound: if the table does not exist
177
+ """
178
+ with self._database.snapshot() as snapshot:
179
+ if not self._exists(snapshot):
180
+ raise NotFound("table '{}' does not exist".format(self.table_id))
181
+ self._schema = self._get_schema(snapshot)
File without changes
@@ -0,0 +1,121 @@
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
+ import grpc
15
+
16
+ from google.api_core import grpc_helpers
17
+ import google.auth.credentials
18
+ from google.cloud.spanner_admin_database_v1 import DatabaseDialect
19
+ from google.cloud.spanner_v1 import SpannerClient
20
+ from google.cloud.spanner_v1.database import Database, SPANNER_DATA_SCOPE
21
+ from google.cloud.spanner_v1.services.spanner.transports import (
22
+ SpannerGrpcTransport,
23
+ SpannerTransport,
24
+ )
25
+ from google.cloud.spanner_v1.testing.interceptors import (
26
+ MethodCountInterceptor,
27
+ MethodAbortInterceptor,
28
+ XGoogRequestIDHeaderInterceptor,
29
+ )
30
+
31
+
32
+ class TestDatabase(Database):
33
+ """Representation of a Cloud Spanner Database. This class is only used for
34
+ system testing as there is no support for interceptors in grpc client
35
+ currently, and we don't want to make changes in the Database class for
36
+ testing purpose as this is a hack to use interceptors in tests."""
37
+
38
+ _interceptors = []
39
+
40
+ def __init__(
41
+ self,
42
+ database_id,
43
+ instance,
44
+ ddl_statements=(),
45
+ pool=None,
46
+ logger=None,
47
+ encryption_config=None,
48
+ database_dialect=DatabaseDialect.DATABASE_DIALECT_UNSPECIFIED,
49
+ database_role=None,
50
+ enable_drop_protection=False,
51
+ ):
52
+ super().__init__(
53
+ database_id,
54
+ instance,
55
+ ddl_statements,
56
+ pool,
57
+ logger,
58
+ encryption_config,
59
+ database_dialect,
60
+ database_role,
61
+ enable_drop_protection,
62
+ )
63
+
64
+ self._method_count_interceptor = MethodCountInterceptor()
65
+ self._method_abort_interceptor = MethodAbortInterceptor()
66
+ self._interceptors = [
67
+ self._method_count_interceptor,
68
+ self._method_abort_interceptor,
69
+ ]
70
+
71
+ @property
72
+ def spanner_api(self):
73
+ """Helper for session-related API calls."""
74
+ if self._spanner_api is None:
75
+ client = self._instance._client
76
+ client_info = client._client_info
77
+ client_options = client._client_options
78
+ if self._instance.emulator_host is not None:
79
+ channel = grpc.insecure_channel(self._instance.emulator_host)
80
+ self._x_goog_request_id_interceptor = XGoogRequestIDHeaderInterceptor()
81
+ self._interceptors.append(self._x_goog_request_id_interceptor)
82
+ channel = grpc.intercept_channel(channel, *self._interceptors)
83
+ transport = SpannerGrpcTransport(channel=channel)
84
+ self._spanner_api = SpannerClient(
85
+ client_info=client_info,
86
+ transport=transport,
87
+ )
88
+ return self._spanner_api
89
+ credentials = client.credentials
90
+ if isinstance(credentials, google.auth.credentials.Scoped):
91
+ credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))
92
+ self._spanner_api = self._create_spanner_client_for_tests(
93
+ client_options,
94
+ credentials,
95
+ )
96
+ return self._spanner_api
97
+
98
+ def _create_spanner_client_for_tests(self, client_options, credentials):
99
+ (
100
+ api_endpoint,
101
+ client_cert_source_func,
102
+ ) = SpannerClient.get_mtls_endpoint_and_cert_source(client_options)
103
+ channel = grpc_helpers.create_channel(
104
+ api_endpoint,
105
+ credentials=credentials,
106
+ credentials_file=client_options.credentials_file,
107
+ quota_project_id=client_options.quota_project_id,
108
+ default_scopes=SpannerTransport.AUTH_SCOPES,
109
+ scopes=client_options.scopes,
110
+ default_host=SpannerTransport.DEFAULT_HOST,
111
+ )
112
+ channel = grpc.intercept_channel(channel, *self._interceptors)
113
+ transport = SpannerGrpcTransport(channel=channel)
114
+ return SpannerClient(
115
+ client_options=client_options,
116
+ transport=transport,
117
+ )
118
+
119
+ def reset(self):
120
+ if self._x_goog_request_id_interceptor:
121
+ self._x_goog_request_id_interceptor.reset()