@@ -186,6 +186,10 @@ def execute(self, sql, args=None):
186186
187187 # Classify whether this is a read-only SQL statement.
188188 try :
189+ if self .connection .read_only :
190+ self ._handle_DQL (sql , args or None )
191+ return
192+
189193 classification = parse_utils .classify_stmt (sql )
190194 if classification == parse_utils .STMT_DDL :
191195 ddl_statements = []
@@ -325,14 +329,15 @@ def fetchone(self):
325329
326330 try :
327331 res = next (self )
328- if not self .connection .autocommit :
332+ if not self .connection .autocommit and not self . connection . read_only :
329333 self ._checksum .consume_result (res )
330334 return res
331335 except StopIteration :
332336 return
333337 except Aborted :
334- self .connection .retry_transaction ()
335- return self .fetchone ()
338+ if not self .connection .read_only :
339+ self .connection .retry_transaction ()
340+ return self .fetchone ()
336341
337342 def fetchall (self ):
338343 """Fetch all (remaining) rows of a query result, returning them as
@@ -343,12 +348,13 @@ def fetchall(self):
343348 res = []
344349 try :
345350 for row in self :
346- if not self .connection .autocommit :
351+ if not self .connection .autocommit and not self . connection . read_only :
347352 self ._checksum .consume_result (row )
348353 res .append (row )
349354 except Aborted :
350- self .connection .retry_transaction ()
351- return self .fetchall ()
355+ if not self .connection .read_only :
356+ self .connection .retry_transaction ()
357+ return self .fetchall ()
352358
353359 return res
354360
@@ -372,14 +378,15 @@ def fetchmany(self, size=None):
372378 for i in range (size ):
373379 try :
374380 res = next (self )
375- if not self .connection .autocommit :
381+ if not self .connection .autocommit and not self . connection . read_only :
376382 self ._checksum .consume_result (res )
377383 items .append (res )
378384 except StopIteration :
379385 break
380386 except Aborted :
381- self .connection .retry_transaction ()
382- return self .fetchmany (size )
387+ if not self .connection .read_only :
388+ self .connection .retry_transaction ()
389+ return self .fetchmany (size )
383390
384391 return items
385392
@@ -395,38 +402,39 @@ def setoutputsize(self, size, column=None):
395402 """A no-op, raising an error if the cursor or connection is closed."""
396403 self ._raise_if_closed ()
397404
405+ def _handle_DQL_with_snapshot (self , snapshot , sql , params ):
406+ # Reference
407+ # https://googleapis.dev/python/spanner/latest/session-api.html#google.cloud.spanner_v1.session.Session.execute_sql
408+ sql , params = parse_utils .sql_pyformat_args_to_spanner (sql , params )
409+ res = snapshot .execute_sql (
410+ sql , params = params , param_types = get_param_types (params )
411+ )
412+ # Immediately using:
413+ # iter(response)
414+ # here, because this Spanner API doesn't provide
415+ # easy mechanisms to detect when only a single item
416+ # is returned or many, yet mixing results that
417+ # are for .fetchone() with those that would result in
418+ # many items returns a RuntimeError if .fetchone() is
419+ # invoked and vice versa.
420+ self ._result_set = res
421+ # Read the first element so that the StreamedResultSet can
422+ # return the metadata after a DQL statement. See issue #155.
423+ self ._itr = PeekIterator (self ._result_set )
424+ # Unfortunately, Spanner doesn't seem to send back
425+ # information about the number of rows available.
426+ self ._row_count = _UNSET_COUNT
427+
398428 def _handle_DQL (self , sql , params ):
399- with self .connection .database .snapshot () as snapshot :
400- # Reference
401- # https://googleapis.dev/python/spanner/latest/session-api.html#google.cloud.spanner_v1.session.Session.execute_sql
402- sql , params = parse_utils .sql_pyformat_args_to_spanner (sql , params )
403- res = snapshot .execute_sql (
404- sql , params = params , param_types = get_param_types (params )
429+ if self .connection .read_only and not self .connection .autocommit :
430+ # initiate or use the existing multi-use snapshot
431+ self ._handle_DQL_with_snapshot (
432+ self .connection .snapshot_checkout (), sql , params
405433 )
406- if type (res ) == int :
407- self ._row_count = res
408- self ._itr = None
409- else :
410- # Immediately using:
411- # iter(response)
412- # here, because this Spanner API doesn't provide
413- # easy mechanisms to detect when only a single item
414- # is returned or many, yet mixing results that
415- # are for .fetchone() with those that would result in
416- # many items returns a RuntimeError if .fetchone() is
417- # invoked and vice versa.
418- self ._result_set = res
419- # Read the first element so that the StreamedResultSet can
420- # return the metadata after a DQL statement. See issue #155.
421- while True :
422- try :
423- self ._itr = PeekIterator (self ._result_set )
424- break
425- except Aborted :
426- self .connection .retry_transaction ()
427- # Unfortunately, Spanner doesn't seem to send back
428- # information about the number of rows available.
429- self ._row_count = _UNSET_COUNT
434+ else :
435+ # execute with single-use snapshot
436+ with self .connection .database .snapshot () as snapshot :
437+ self ._handle_DQL_with_snapshot (snapshot , sql , params )
430438
431439 def __enter__ (self ):
432440 return self
0 commit comments