@@ -34,6 +34,10 @@ class _SnapshotBase(_SessionWrapper):
34
34
:type session: :class:`~google.cloud.spanner.session.Session`
35
35
:param session: the session used to perform the commit
36
36
"""
37
+ _multi_use = False
38
+ _transaction_id = None
39
+ _read_request_count = 0
40
+
37
41
def _make_txn_selector (self ): # pylint: disable=redundant-returns-doc
38
42
"""Helper for :meth:`read` / :meth:`execute_sql`.
39
43
@@ -70,7 +74,15 @@ def read(self, table, columns, keyset, index='', limit=0,
70
74
71
75
:rtype: :class:`~google.cloud.spanner.streamed.StreamedResultSet`
72
76
:returns: a result set instance which can be used to consume rows.
77
+ :raises: ValueError for reuse of single-use snapshots, or if a
78
+ transaction ID is pending for multiple-use snapshots.
73
79
"""
80
+ if self ._read_request_count > 0 :
81
+ if not self ._multi_use :
82
+ raise ValueError ("Cannot re-use single-use snapshot." )
83
+ if self ._transaction_id is None :
84
+ raise ValueError ("Transaction ID pending." )
85
+
74
86
database = self ._session ._database
75
87
api = database .spanner_api
76
88
options = _options_with_prefix (database .name )
@@ -81,7 +93,12 @@ def read(self, table, columns, keyset, index='', limit=0,
81
93
transaction = transaction , index = index , limit = limit ,
82
94
resume_token = resume_token , options = options )
83
95
84
- return StreamedResultSet (iterator )
96
+ self ._read_request_count += 1
97
+
98
+ if self ._multi_use :
99
+ return StreamedResultSet (iterator , source = self )
100
+ else :
101
+ return StreamedResultSet (iterator )
85
102
86
103
def execute_sql (self , sql , params = None , param_types = None , query_mode = None ,
87
104
resume_token = b'' ):
@@ -109,7 +126,15 @@ def execute_sql(self, sql, params=None, param_types=None, query_mode=None,
109
126
110
127
:rtype: :class:`~google.cloud.spanner.streamed.StreamedResultSet`
111
128
:returns: a result set instance which can be used to consume rows.
129
+ :raises: ValueError for reuse of single-use snapshots, or if a
130
+ transaction ID is pending for multiple-use snapshots.
112
131
"""
132
+ if self ._read_request_count > 0 :
133
+ if not self ._multi_use :
134
+ raise ValueError ("Cannot re-use single-use snapshot." )
135
+ if self ._transaction_id is None :
136
+ raise ValueError ("Transaction ID pending." )
137
+
113
138
if params is not None :
114
139
if param_types is None :
115
140
raise ValueError (
@@ -128,7 +153,12 @@ def execute_sql(self, sql, params=None, param_types=None, query_mode=None,
128
153
transaction = transaction , params = params_pb , param_types = param_types ,
129
154
query_mode = query_mode , resume_token = resume_token , options = options )
130
155
131
- return StreamedResultSet (iterator )
156
+ self ._read_request_count += 1
157
+
158
+ if self ._multi_use :
159
+ return StreamedResultSet (iterator , source = self )
160
+ else :
161
+ return StreamedResultSet (iterator )
132
162
133
163
134
164
class Snapshot (_SnapshotBase ):
@@ -157,9 +187,16 @@ class Snapshot(_SnapshotBase):
157
187
:type exact_staleness: :class:`datetime.timedelta`
158
188
:param exact_staleness: Execute all reads at a timestamp that is
159
189
``exact_staleness`` old.
190
+
191
+ :type multi_use: :class:`bool`
192
+ :param multi_use: If true, multipl :meth:`read` / :meth:`execute_sql`
193
+ calls can be performed with the snapshot in the
194
+ context of a read-only transaction, used to ensure
195
+ isolation / consistency. Incompatible with
196
+ ``max_staleness`` and ``min_read_timestamp``.
160
197
"""
161
198
def __init__ (self , session , read_timestamp = None , min_read_timestamp = None ,
162
- max_staleness = None , exact_staleness = None ):
199
+ max_staleness = None , exact_staleness = None , multi_use = False ):
163
200
super (Snapshot , self ).__init__ (session )
164
201
opts = [
165
202
read_timestamp , min_read_timestamp , max_staleness , exact_staleness ]
@@ -168,14 +205,24 @@ def __init__(self, session, read_timestamp=None, min_read_timestamp=None,
168
205
if len (flagged ) > 1 :
169
206
raise ValueError ("Supply zero or one options." )
170
207
208
+ if multi_use :
209
+ if min_read_timestamp is not None or max_staleness is not None :
210
+ raise ValueError (
211
+ "'multi_use' is incompatible with "
212
+ "'min_read_timestamp' / 'max_staleness'" )
213
+
171
214
self ._strong = len (flagged ) == 0
172
215
self ._read_timestamp = read_timestamp
173
216
self ._min_read_timestamp = min_read_timestamp
174
217
self ._max_staleness = max_staleness
175
218
self ._exact_staleness = exact_staleness
219
+ self ._multi_use = multi_use
176
220
177
221
def _make_txn_selector (self ):
178
222
"""Helper for :meth:`read`."""
223
+ if self ._transaction_id is not None :
224
+ return TransactionSelector (id = self ._transaction_id )
225
+
179
226
if self ._read_timestamp :
180
227
key = 'read_timestamp'
181
228
value = _datetime_to_pb_timestamp (self ._read_timestamp )
@@ -194,4 +241,34 @@ def _make_txn_selector(self):
194
241
195
242
options = TransactionOptions (
196
243
read_only = TransactionOptions .ReadOnly (** {key : value }))
197
- return TransactionSelector (single_use = options )
244
+
245
+ if self ._multi_use :
246
+ return TransactionSelector (begin = options )
247
+ else :
248
+ return TransactionSelector (single_use = options )
249
+
250
+ def begin (self ):
251
+ """Begin a transaction on the database.
252
+
253
+ :rtype: bytes
254
+ :returns: the ID for the newly-begun transaction.
255
+ :raises: ValueError if the transaction is already begun, committed,
256
+ or rolled back.
257
+ """
258
+ if not self ._multi_use :
259
+ raise ValueError ("Cannot call 'begin' single-use snapshots" )
260
+
261
+ if self ._transaction_id is not None :
262
+ raise ValueError ("Read-only transaction already begun" )
263
+
264
+ if self ._read_request_count > 0 :
265
+ raise ValueError ("Read-only transaction already pending" )
266
+
267
+ database = self ._session ._database
268
+ api = database .spanner_api
269
+ options = _options_with_prefix (database .name )
270
+ txn_selector = self ._make_txn_selector ()
271
+ response = api .begin_transaction (
272
+ self ._session .name , txn_selector .begin , options = options )
273
+ self ._transaction_id = response .id
274
+ return self ._transaction_id
0 commit comments