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,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()
|