@@ -186,6 +186,10 @@ def execute(self, sql, args=None):
186
186
187
187
# Classify whether this is a read-only SQL statement.
188
188
try :
189
+ if self .connection .read_only :
190
+ self ._handle_DQL (sql , args or None )
191
+ return
192
+
189
193
classification = parse_utils .classify_stmt (sql )
190
194
if classification == parse_utils .STMT_DDL :
191
195
ddl_statements = []
@@ -325,14 +329,15 @@ def fetchone(self):
325
329
326
330
try :
327
331
res = next (self )
328
- if not self .connection .autocommit :
332
+ if not self .connection .autocommit and not self . connection . read_only :
329
333
self ._checksum .consume_result (res )
330
334
return res
331
335
except StopIteration :
332
336
return
333
337
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 ()
336
341
337
342
def fetchall (self ):
338
343
"""Fetch all (remaining) rows of a query result, returning them as
@@ -343,12 +348,13 @@ def fetchall(self):
343
348
res = []
344
349
try :
345
350
for row in self :
346
- if not self .connection .autocommit :
351
+ if not self .connection .autocommit and not self . connection . read_only :
347
352
self ._checksum .consume_result (row )
348
353
res .append (row )
349
354
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 ()
352
358
353
359
return res
354
360
@@ -372,14 +378,15 @@ def fetchmany(self, size=None):
372
378
for i in range (size ):
373
379
try :
374
380
res = next (self )
375
- if not self .connection .autocommit :
381
+ if not self .connection .autocommit and not self . connection . read_only :
376
382
self ._checksum .consume_result (res )
377
383
items .append (res )
378
384
except StopIteration :
379
385
break
380
386
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 )
383
390
384
391
return items
385
392
@@ -395,38 +402,39 @@ def setoutputsize(self, size, column=None):
395
402
"""A no-op, raising an error if the cursor or connection is closed."""
396
403
self ._raise_if_closed ()
397
404
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
+
398
428
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
405
433
)
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 )
430
438
431
439
def __enter__ (self ):
432
440
return self
0 commit comments