Skip to content

Commit 230d715

Browse files
committed
Add 'Snapshot.begin' API method.
- Valid only for multi-use snapshots. - Raises if the snapshot already has a transaction ID.
1 parent a5219a5 commit 230d715

2 files changed

Lines changed: 119 additions & 4 deletions

File tree

spanner/google/cloud/spanner/snapshot.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,26 @@ def _make_txn_selector(self):
225225
return TransactionSelector(begin=options)
226226
else:
227227
return TransactionSelector(single_use=options)
228+
229+
def begin(self):
230+
"""Begin a transaction on the database.
231+
232+
:rtype: bytes
233+
:returns: the ID for the newly-begun transaction.
234+
:raises: ValueError if the transaction is already begun, committed,
235+
or rolled back.
236+
"""
237+
if not self._multi_use:
238+
raise ValueError("Cannot call 'begin' single-use snapshots")
239+
240+
if self._transaction_id is not None:
241+
raise ValueError("Transaction already begun")
242+
243+
database = self._session._database
244+
api = database.spanner_api
245+
options = _options_with_prefix(database.name)
246+
txn_selector = self._make_txn_selector()
247+
response = api.begin_transaction(
248+
self._session.name, txn_selector.begin, options=options)
249+
self._transaction_id = response.id
250+
return self._transaction_id

spanner/tests/unit/test_snapshot.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ class TestSnapshot(unittest.TestCase):
353353
DATABASE_NAME = INSTANCE_NAME + '/databases/' + DATABASE_ID
354354
SESSION_ID = 'session-id'
355355
SESSION_NAME = DATABASE_NAME + '/sessions/' + SESSION_ID
356+
TRANSACTION_ID = b'DEADBEEF'
356357

357358
def _getTargetClass(self):
358359
from google.cloud.spanner.snapshot import Snapshot
@@ -493,12 +494,11 @@ def test_ctor_w_multi_use_and_exact_staleness(self):
493494
self.assertTrue(snapshot._multi_use)
494495

495496
def test__make_txn_selector_w_transaction_id(self):
496-
TXN_ID = b'DEADBEEF'
497497
session = _Session()
498498
snapshot = self._make_one(session)
499-
snapshot._transaction_id = TXN_ID
499+
snapshot._transaction_id = self.TRANSACTION_ID
500500
selector = snapshot._make_txn_selector()
501-
self.assertEqual(selector.id, TXN_ID)
501+
self.assertEqual(selector.id, self.TRANSACTION_ID)
502502

503503
def test__make_txn_selector_strong(self):
504504
session = _Session()
@@ -579,6 +579,90 @@ def test__make_txn_selector_w_exact_staleness_w_multi_use(self):
579579
self.assertEqual(options.read_only.exact_staleness.seconds, 3)
580580
self.assertEqual(options.read_only.exact_staleness.nanos, 123456000)
581581

582+
def test_begin_wo_multi_use(self):
583+
session = _Session()
584+
snapshot = self._make_one(session)
585+
with self.assertRaises(ValueError):
586+
snapshot.begin()
587+
588+
def test_begin_w_existing_txn_id(self):
589+
session = _Session()
590+
snapshot = self._make_one(session, multi_use=True)
591+
snapshot._transaction_id = self.TRANSACTION_ID
592+
with self.assertRaises(ValueError):
593+
snapshot.begin()
594+
595+
def test_begin_w_gax_error(self):
596+
from google.gax.errors import GaxError
597+
from google.cloud._helpers import _pb_timestamp_to_datetime
598+
599+
database = _Database()
600+
api = database.spanner_api = _FauxSpannerAPI(
601+
_random_gax_error=True)
602+
timestamp = self._makeTimestamp()
603+
session = _Session(database)
604+
snapshot = self._make_one(
605+
session, read_timestamp=timestamp, multi_use=True)
606+
607+
with self.assertRaises(GaxError):
608+
snapshot.begin()
609+
610+
session_id, txn_options, options = api._begun
611+
self.assertEqual(session_id, session.name)
612+
self.assertEqual(
613+
_pb_timestamp_to_datetime(txn_options.read_only.read_timestamp),
614+
timestamp)
615+
self.assertEqual(options.kwargs['metadata'],
616+
[('google-cloud-resource-prefix', database.name)])
617+
618+
def test_begin_ok_exact_staleness(self):
619+
from google.cloud.proto.spanner.v1.transaction_pb2 import (
620+
Transaction as TransactionPB)
621+
622+
transaction_pb = TransactionPB(id=self.TRANSACTION_ID)
623+
database = _Database()
624+
api = database.spanner_api = _FauxSpannerAPI(
625+
_begin_transaction_response=transaction_pb)
626+
duration = self._makeDuration(seconds=3, microseconds=123456)
627+
session = _Session(database)
628+
snapshot = self._make_one(
629+
session, exact_staleness=duration, multi_use=True)
630+
631+
txn_id = snapshot.begin()
632+
633+
self.assertEqual(txn_id, self.TRANSACTION_ID)
634+
self.assertEqual(snapshot._transaction_id, self.TRANSACTION_ID)
635+
636+
session_id, txn_options, options = api._begun
637+
self.assertEqual(session_id, session.name)
638+
read_only = txn_options.read_only
639+
self.assertEqual(read_only.exact_staleness.seconds, 3)
640+
self.assertEqual(read_only.exact_staleness.nanos, 123456000)
641+
self.assertEqual(options.kwargs['metadata'],
642+
[('google-cloud-resource-prefix', database.name)])
643+
644+
def test_begin_ok_exact_strong(self):
645+
from google.cloud.proto.spanner.v1.transaction_pb2 import (
646+
Transaction as TransactionPB)
647+
648+
transaction_pb = TransactionPB(id=self.TRANSACTION_ID)
649+
database = _Database()
650+
api = database.spanner_api = _FauxSpannerAPI(
651+
_begin_transaction_response=transaction_pb)
652+
session = _Session(database)
653+
snapshot = self._make_one(session, multi_use=True)
654+
655+
txn_id = snapshot.begin()
656+
657+
self.assertEqual(txn_id, self.TRANSACTION_ID)
658+
self.assertEqual(snapshot._transaction_id, self.TRANSACTION_ID)
659+
660+
session_id, txn_options, options = api._begun
661+
self.assertEqual(session_id, session.name)
662+
self.assertTrue(txn_options.read_only.strong)
663+
self.assertEqual(options.kwargs['metadata'],
664+
[('google-cloud-resource-prefix', database.name)])
665+
582666

583667
class _Session(object):
584668

@@ -593,7 +677,15 @@ class _Database(object):
593677

594678
class _FauxSpannerAPI(_GAXBaseAPI):
595679

596-
_read_with = None
680+
_read_with = _begin = None
681+
682+
def begin_transaction(self, session, options_, options=None):
683+
from google.gax.errors import GaxError
684+
685+
self._begun = (session, options_, options)
686+
if self._random_gax_error:
687+
raise GaxError('error')
688+
return self._begin_transaction_response
597689

598690
# pylint: disable=too-many-arguments
599691
def streaming_read(self, session, table, columns, key_set,

0 commit comments

Comments
 (0)