@@ -34,6 +34,10 @@ class _SnapshotBase(_SessionWrapper):
3434 :type session: :class:`~google.cloud.spanner.session.Session`
3535 :param session: the session used to perform the commit
3636 """
37+ _multi_use = False
38+ _transaction_id = None
39+ _read_request_count = 0
40+
3741 def _make_txn_selector (self ): # pylint: disable=redundant-returns-doc
3842 """Helper for :meth:`read` / :meth:`execute_sql`.
3943
@@ -70,7 +74,15 @@ def read(self, table, columns, keyset, index='', limit=0,
7074
7175 :rtype: :class:`~google.cloud.spanner.streamed.StreamedResultSet`
7276 :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.
7379 """
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+
7486 database = self ._session ._database
7587 api = database .spanner_api
7688 options = _options_with_prefix (database .name )
@@ -81,7 +93,12 @@ def read(self, table, columns, keyset, index='', limit=0,
8193 transaction = transaction , index = index , limit = limit ,
8294 resume_token = resume_token , options = options )
8395
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 )
85102
86103 def execute_sql (self , sql , params = None , param_types = None , query_mode = None ,
87104 resume_token = b'' ):
@@ -109,7 +126,15 @@ def execute_sql(self, sql, params=None, param_types=None, query_mode=None,
109126
110127 :rtype: :class:`~google.cloud.spanner.streamed.StreamedResultSet`
111128 :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.
112131 """
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+
113138 if params is not None :
114139 if param_types is None :
115140 raise ValueError (
@@ -128,7 +153,12 @@ def execute_sql(self, sql, params=None, param_types=None, query_mode=None,
128153 transaction = transaction , params = params_pb , param_types = param_types ,
129154 query_mode = query_mode , resume_token = resume_token , options = options )
130155
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 )
132162
133163
134164class Snapshot (_SnapshotBase ):
@@ -157,9 +187,16 @@ class Snapshot(_SnapshotBase):
157187 :type exact_staleness: :class:`datetime.timedelta`
158188 :param exact_staleness: Execute all reads at a timestamp that is
159189 ``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``.
160197 """
161198 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 ):
163200 super (Snapshot , self ).__init__ (session )
164201 opts = [
165202 read_timestamp , min_read_timestamp , max_staleness , exact_staleness ]
@@ -168,14 +205,24 @@ def __init__(self, session, read_timestamp=None, min_read_timestamp=None,
168205 if len (flagged ) > 1 :
169206 raise ValueError ("Supply zero or one options." )
170207
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+
171214 self ._strong = len (flagged ) == 0
172215 self ._read_timestamp = read_timestamp
173216 self ._min_read_timestamp = min_read_timestamp
174217 self ._max_staleness = max_staleness
175218 self ._exact_staleness = exact_staleness
219+ self ._multi_use = multi_use
176220
177221 def _make_txn_selector (self ):
178222 """Helper for :meth:`read`."""
223+ if self ._transaction_id is not None :
224+ return TransactionSelector (id = self ._transaction_id )
225+
179226 if self ._read_timestamp :
180227 key = 'read_timestamp'
181228 value = _datetime_to_pb_timestamp (self ._read_timestamp )
@@ -194,4 +241,34 @@ def _make_txn_selector(self):
194241
195242 options = TransactionOptions (
196243 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