Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit b5a7786

Browse files
authored
Merge branch 'main' into parthea-patch-3
2 parents 1afbeec + 9331146 commit b5a7786

32 files changed

+1396
-263
lines changed

.github/.OwlBot.lock.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
# limitations under the License.
1414
docker:
1515
image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
16-
digest: sha256:5581906b957284864632cde4e9c51d1cc66b0094990b27e689132fe5cd036046
17-
# created: 2025-03-05
16+
digest: sha256:25de45b58e52021d3a24a6273964371a97a4efeefe6ad3845a64e697c63b6447
17+
# created: 2025-04-14T14:34:43.260858345Z

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "3.53.0"
2+
".": "3.54.0"
33
}

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
[1]: https://pypi.org/project/google-cloud-spanner/#history
66

7+
## [3.54.0](https://github.com/googleapis/python-spanner/compare/v3.53.0...v3.54.0) (2025-04-28)
8+
9+
10+
### Features
11+
12+
* Add interval type support ([#1340](https://github.com/googleapis/python-spanner/issues/1340)) ([6ca9b43](https://github.com/googleapis/python-spanner/commit/6ca9b43c3038eca1317c7c9b7e3543b5f1bc68ad))
13+
* Add sample for pre-split feature ([#1333](https://github.com/googleapis/python-spanner/issues/1333)) ([ca76108](https://github.com/googleapis/python-spanner/commit/ca76108809174e4f3eea38d7ac2463d9b4c73304))
14+
* Add SQL statement for begin transaction isolation level ([#1331](https://github.com/googleapis/python-spanner/issues/1331)) ([3ac0f91](https://github.com/googleapis/python-spanner/commit/3ac0f9131b38e5cfb2b574d3d73b03736b871712))
15+
* Support transaction isolation level in dbapi ([#1327](https://github.com/googleapis/python-spanner/issues/1327)) ([03400c4](https://github.com/googleapis/python-spanner/commit/03400c40f1c1cc73e51733f2a28910a8dd78e7d9))
16+
17+
18+
### Bug Fixes
19+
20+
* Improve client-side regex statement parser ([#1328](https://github.com/googleapis/python-spanner/issues/1328)) ([b3c259d](https://github.com/googleapis/python-spanner/commit/b3c259deec817812fd8e4940faacf4a927d0d69c))
21+
722
## [3.53.0](https://github.com/googleapis/python-spanner/compare/v3.52.0...v3.53.0) (2025-03-12)
823

924

google/cloud/spanner_admin_database_v1/gapic_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
__version__ = "3.53.0" # {x-release-please-version}
16+
__version__ = "3.54.0" # {x-release-please-version}

google/cloud/spanner_admin_instance_v1/gapic_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
__version__ = "3.53.0" # {x-release-please-version}
16+
__version__ = "3.54.0" # {x-release-please-version}

google/cloud/spanner_dbapi/batch_dml_executor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ def execute_statement(self, parsed_statement: ParsedStatement):
5454
"""
5555
from google.cloud.spanner_dbapi import ProgrammingError
5656

57+
# Note: Let the server handle it if the client-side parser did not
58+
# recognize the type of statement.
5759
if (
5860
parsed_statement.statement_type != StatementType.UPDATE
5961
and parsed_statement.statement_type != StatementType.INSERT
62+
and parsed_statement.statement_type != StatementType.UNKNOWN
6063
):
6164
raise ProgrammingError("Only DML statements are allowed in batch DML mode.")
6265
self._statements.append(parsed_statement.statement)

google/cloud/spanner_dbapi/client_side_statement_executor.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from typing import TYPE_CHECKING
14+
from typing import TYPE_CHECKING, Union
15+
from google.cloud.spanner_v1 import TransactionOptions
1516

1617
if TYPE_CHECKING:
1718
from google.cloud.spanner_dbapi.cursor import Cursor
@@ -58,7 +59,7 @@ def execute(cursor: "Cursor", parsed_statement: ParsedStatement):
5859
connection.commit()
5960
return None
6061
if statement_type == ClientSideStatementType.BEGIN:
61-
connection.begin()
62+
connection.begin(isolation_level=_get_isolation_level(parsed_statement))
6263
return None
6364
if statement_type == ClientSideStatementType.ROLLBACK:
6465
connection.rollback()
@@ -121,3 +122,19 @@ def _get_streamed_result_set(column_name, type_code, column_values):
121122
column_values_pb.append(_make_value_pb(column_value))
122123
result_set.values.extend(column_values_pb)
123124
return StreamedResultSet(iter([result_set]))
125+
126+
127+
def _get_isolation_level(
128+
statement: ParsedStatement,
129+
) -> Union[TransactionOptions.IsolationLevel, None]:
130+
if (
131+
statement.client_side_statement_params is None
132+
or len(statement.client_side_statement_params) == 0
133+
):
134+
return None
135+
level = statement.client_side_statement_params[0]
136+
if not isinstance(level, str) or level == "":
137+
return None
138+
# Replace (duplicate) whitespaces in the string with an underscore.
139+
level = "_".join(level.split()).upper()
140+
return TransactionOptions.IsolationLevel[level]

google/cloud/spanner_dbapi/client_side_statement_parser.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@
2121
Statement,
2222
)
2323

24-
RE_BEGIN = re.compile(r"^\s*(BEGIN|START)(TRANSACTION)?", re.IGNORECASE)
25-
RE_COMMIT = re.compile(r"^\s*(COMMIT)(TRANSACTION)?", re.IGNORECASE)
26-
RE_ROLLBACK = re.compile(r"^\s*(ROLLBACK)(TRANSACTION)?", re.IGNORECASE)
24+
RE_BEGIN = re.compile(
25+
r"^\s*(?:BEGIN|START)(?:\s+TRANSACTION)?(?:\s+ISOLATION\s+LEVEL\s+(REPEATABLE\s+READ|SERIALIZABLE))?\s*$",
26+
re.IGNORECASE,
27+
)
28+
RE_COMMIT = re.compile(r"^\s*(COMMIT)(\s+TRANSACTION)?\s*$", re.IGNORECASE)
29+
RE_ROLLBACK = re.compile(r"^\s*(ROLLBACK)(\s+TRANSACTION)?\s*$", re.IGNORECASE)
2730
RE_SHOW_COMMIT_TIMESTAMP = re.compile(
28-
r"^\s*(SHOW)\s+(VARIABLE)\s+(COMMIT_TIMESTAMP)", re.IGNORECASE
31+
r"^\s*(SHOW)\s+(VARIABLE)\s+(COMMIT_TIMESTAMP)\s*$", re.IGNORECASE
2932
)
3033
RE_SHOW_READ_TIMESTAMP = re.compile(
31-
r"^\s*(SHOW)\s+(VARIABLE)\s+(READ_TIMESTAMP)", re.IGNORECASE
34+
r"^\s*(SHOW)\s+(VARIABLE)\s+(READ_TIMESTAMP)\s*$", re.IGNORECASE
3235
)
33-
RE_START_BATCH_DML = re.compile(r"^\s*(START)\s+(BATCH)\s+(DML)", re.IGNORECASE)
34-
RE_RUN_BATCH = re.compile(r"^\s*(RUN)\s+(BATCH)", re.IGNORECASE)
35-
RE_ABORT_BATCH = re.compile(r"^\s*(ABORT)\s+(BATCH)", re.IGNORECASE)
36+
RE_START_BATCH_DML = re.compile(r"^\s*(START)\s+(BATCH)\s+(DML)\s*$", re.IGNORECASE)
37+
RE_RUN_BATCH = re.compile(r"^\s*(RUN)\s+(BATCH)\s*$", re.IGNORECASE)
38+
RE_ABORT_BATCH = re.compile(r"^\s*(ABORT)\s+(BATCH)\s*$", re.IGNORECASE)
3639
RE_PARTITION_QUERY = re.compile(r"^\s*(PARTITION)\s+(.+)", re.IGNORECASE)
3740
RE_RUN_PARTITION = re.compile(r"^\s*(RUN)\s+(PARTITION)\s+(.+)", re.IGNORECASE)
3841
RE_RUN_PARTITIONED_QUERY = re.compile(
@@ -68,6 +71,10 @@ def parse_stmt(query):
6871
elif RE_START_BATCH_DML.match(query):
6972
client_side_statement_type = ClientSideStatementType.START_BATCH_DML
7073
elif RE_BEGIN.match(query):
74+
match = re.search(RE_BEGIN, query)
75+
isolation_level = match.group(1)
76+
if isolation_level is not None:
77+
client_side_statement_params.append(isolation_level)
7178
client_side_statement_type = ClientSideStatementType.BEGIN
7279
elif RE_RUN_BATCH.match(query):
7380
client_side_statement_type = ClientSideStatementType.RUN_BATCH

google/cloud/spanner_dbapi/connection.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,12 @@
2020
from google.cloud import spanner_v1 as spanner
2121
from google.cloud.spanner_dbapi import partition_helper
2222
from google.cloud.spanner_dbapi.batch_dml_executor import BatchMode, BatchDmlExecutor
23-
from google.cloud.spanner_dbapi.parse_utils import _get_statement_type
24-
from google.cloud.spanner_dbapi.parsed_statement import (
25-
StatementType,
26-
AutocommitDmlMode,
27-
)
23+
from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode
2824
from google.cloud.spanner_dbapi.partition_helper import PartitionId
2925
from google.cloud.spanner_dbapi.parsed_statement import ParsedStatement, Statement
3026
from google.cloud.spanner_dbapi.transaction_helper import TransactionRetryHelper
3127
from google.cloud.spanner_dbapi.cursor import Cursor
32-
from google.cloud.spanner_v1 import RequestOptions
28+
from google.cloud.spanner_v1 import RequestOptions, TransactionOptions
3329
from google.cloud.spanner_v1.snapshot import Snapshot
3430

3531
from google.cloud.spanner_dbapi.exceptions import (
@@ -112,6 +108,7 @@ def __init__(self, instance, database=None, read_only=False, **kwargs):
112108
self._staleness = None
113109
self.request_priority = None
114110
self._transaction_begin_marked = False
111+
self._transaction_isolation_level = None
115112
# whether transaction started at Spanner. This means that we had
116113
# made at least one call to Spanner.
117114
self._spanner_transaction_started = False
@@ -283,6 +280,33 @@ def transaction_tag(self, value):
283280
"""
284281
self._connection_variables["transaction_tag"] = value
285282

283+
@property
284+
def isolation_level(self):
285+
"""The default isolation level that is used for all read/write
286+
transactions on this `Connection`.
287+
288+
Returns:
289+
google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel:
290+
The isolation level that is used for read/write transactions on
291+
this `Connection`.
292+
"""
293+
return self._connection_variables.get(
294+
"isolation_level",
295+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
296+
)
297+
298+
@isolation_level.setter
299+
def isolation_level(self, value: TransactionOptions.IsolationLevel):
300+
"""Sets the isolation level that is used for all read/write
301+
transactions on this `Connection`.
302+
303+
Args:
304+
value (google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel):
305+
The isolation level for all read/write transactions on this
306+
`Connection`.
307+
"""
308+
self._connection_variables["isolation_level"] = value
309+
286310
@property
287311
def staleness(self):
288312
"""Current read staleness option value of this `Connection`.
@@ -363,6 +387,12 @@ def transaction_checkout(self):
363387
if not self._spanner_transaction_started:
364388
self._transaction = self._session_checkout().transaction()
365389
self._transaction.transaction_tag = self.transaction_tag
390+
if self._transaction_isolation_level:
391+
self._transaction.isolation_level = (
392+
self._transaction_isolation_level
393+
)
394+
else:
395+
self._transaction.isolation_level = self.isolation_level
366396
self.transaction_tag = None
367397
self._snapshot = None
368398
self._spanner_transaction_started = True
@@ -405,7 +435,7 @@ def close(self):
405435
self.is_closed = True
406436

407437
@check_not_closed
408-
def begin(self):
438+
def begin(self, isolation_level=None):
409439
"""
410440
Marks the transaction as started.
411441
@@ -421,6 +451,7 @@ def begin(self):
421451
"is already running"
422452
)
423453
self._transaction_begin_marked = True
454+
self._transaction_isolation_level = isolation_level
424455

425456
def commit(self):
426457
"""Commits any pending transaction to the database.
@@ -465,6 +496,7 @@ def _reset_post_commit_or_rollback(self):
465496
self._release_session()
466497
self._transaction_helper.reset()
467498
self._transaction_begin_marked = False
499+
self._transaction_isolation_level = None
468500
self._spanner_transaction_started = False
469501

470502
@check_not_closed
@@ -666,10 +698,6 @@ def set_autocommit_dml_mode(
666698
self._autocommit_dml_mode = autocommit_dml_mode
667699

668700
def _partitioned_query_validation(self, partitioned_query, statement):
669-
if _get_statement_type(Statement(partitioned_query)) is not StatementType.QUERY:
670-
raise ProgrammingError(
671-
"Only queries can be partitioned. Invalid statement: " + statement.sql
672-
)
673701
if self.read_only is not True and self._client_transaction_started is True:
674702
raise ProgrammingError(
675703
"Partitioned query is not supported, because the connection is in a read/write transaction."

google/cloud/spanner_dbapi/cursor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,12 @@ def executemany(self, operation, seq_of_params):
404404
# For every operation, we've got to ensure that any prior DDL
405405
# statements were run.
406406
self.connection.run_prior_DDL_statements()
407+
# Treat UNKNOWN statements as if they are DML and let the server
408+
# determine what is wrong with it.
407409
if self._parsed_statement.statement_type in (
408410
StatementType.INSERT,
409411
StatementType.UPDATE,
412+
StatementType.UNKNOWN,
410413
):
411414
statements = []
412415
for params in seq_of_params:

0 commit comments

Comments
 (0)