Skip to content

Commit 3aab0ed

Browse files
authored
feat: support partitioned dml in dbapi (#1103)
* feat: Implementation to support executing partitioned dml query at dbapi * Small fix * Comments incorporated * Comments incorporated
1 parent 3669303 commit 3aab0ed

File tree

8 files changed

+171
-0
lines changed

8 files changed

+171
-0
lines changed

google/cloud/spanner_dbapi/client_side_statement_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def execute(cursor: "Cursor", parsed_statement: ParsedStatement):
105105
)
106106
if statement_type == ClientSideStatementType.RUN_PARTITIONED_QUERY:
107107
return connection.run_partitioned_query(parsed_statement)
108+
if statement_type == ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE:
109+
return connection._set_autocommit_dml_mode(parsed_statement)
108110

109111

110112
def _get_streamed_result_set(column_name, type_code, column_values):

google/cloud/spanner_dbapi/client_side_statement_parser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
RE_RUN_PARTITIONED_QUERY = re.compile(
3939
r"^\s*(RUN)\s+(PARTITIONED)\s+(QUERY)\s+(.+)", re.IGNORECASE
4040
)
41+
RE_SET_AUTOCOMMIT_DML_MODE = re.compile(
42+
r"^\s*(SET)\s+(AUTOCOMMIT_DML_MODE)\s+(=)\s+(.+)", re.IGNORECASE
43+
)
4144

4245

4346
def parse_stmt(query):
@@ -82,6 +85,10 @@ def parse_stmt(query):
8285
match = re.search(RE_RUN_PARTITION, query)
8386
client_side_statement_params.append(match.group(3))
8487
client_side_statement_type = ClientSideStatementType.RUN_PARTITION
88+
elif RE_SET_AUTOCOMMIT_DML_MODE.match(query):
89+
match = re.search(RE_SET_AUTOCOMMIT_DML_MODE, query)
90+
client_side_statement_params.append(match.group(4))
91+
client_side_statement_type = ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE
8592
if client_side_statement_type is not None:
8693
return ParsedStatement(
8794
StatementType.CLIENT_SIDE,

google/cloud/spanner_dbapi/connection.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from google.cloud.spanner_dbapi.parse_utils import _get_statement_type
2424
from google.cloud.spanner_dbapi.parsed_statement import (
2525
StatementType,
26+
AutocommitDmlMode,
2627
)
2728
from google.cloud.spanner_dbapi.partition_helper import PartitionId
2829
from google.cloud.spanner_dbapi.parsed_statement import ParsedStatement, Statement
@@ -116,6 +117,7 @@ def __init__(self, instance, database=None, read_only=False):
116117
self._batch_mode = BatchMode.NONE
117118
self._batch_dml_executor: BatchDmlExecutor = None
118119
self._transaction_helper = TransactionRetryHelper(self)
120+
self._autocommit_dml_mode: AutocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL
119121

120122
@property
121123
def spanner_client(self):
@@ -167,6 +169,23 @@ def database(self):
167169
"""
168170
return self._database
169171

172+
@property
173+
def autocommit_dml_mode(self):
174+
"""Modes for executing DML statements in autocommit mode for this connection.
175+
176+
The DML autocommit modes are:
177+
1) TRANSACTIONAL - DML statements are executed as single read-write transaction.
178+
After successful execution, the DML statement is guaranteed to have been applied
179+
exactly once to the database.
180+
181+
2) PARTITIONED_NON_ATOMIC - DML statements are executed as partitioned DML transactions.
182+
If an error occurs during the execution of the DML statement, it is possible that the
183+
statement has been applied to some but not all of the rows specified in the statement.
184+
185+
:rtype: :class:`~google.cloud.spanner_dbapi.parsed_statement.AutocommitDmlMode`
186+
"""
187+
return self._autocommit_dml_mode
188+
170189
@property
171190
@deprecated(
172191
reason="This method is deprecated. Use _spanner_transaction_started field"
@@ -577,6 +596,37 @@ def run_partitioned_query(
577596
partitioned_query, statement.params, statement.param_types
578597
)
579598

599+
@check_not_closed
600+
def _set_autocommit_dml_mode(
601+
self,
602+
parsed_statement: ParsedStatement,
603+
):
604+
autocommit_dml_mode_str = parsed_statement.client_side_statement_params[0]
605+
autocommit_dml_mode = AutocommitDmlMode[autocommit_dml_mode_str.upper()]
606+
self.set_autocommit_dml_mode(autocommit_dml_mode)
607+
608+
def set_autocommit_dml_mode(
609+
self,
610+
autocommit_dml_mode,
611+
):
612+
"""
613+
Sets the mode for executing DML statements in autocommit mode for this connection.
614+
This mode is only used when the connection is in autocommit mode, and may only
615+
be set while the transaction is in autocommit mode and not in a temporary transaction.
616+
"""
617+
618+
if self._client_transaction_started is True:
619+
raise ProgrammingError(
620+
"Cannot set autocommit DML mode while not in autocommit mode or while a transaction is active."
621+
)
622+
if self.read_only is True:
623+
raise ProgrammingError(
624+
"Cannot set autocommit DML mode for a read-only connection."
625+
)
626+
if self._batch_mode is not BatchMode.NONE:
627+
raise ProgrammingError("Cannot set autocommit DML mode while in a batch.")
628+
self._autocommit_dml_mode = autocommit_dml_mode
629+
580630
def _partitioned_query_validation(self, partitioned_query, statement):
581631
if _get_statement_type(Statement(partitioned_query)) is not StatementType.QUERY:
582632
raise ProgrammingError(

google/cloud/spanner_dbapi/cursor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
StatementType,
4646
Statement,
4747
ParsedStatement,
48+
AutocommitDmlMode,
4849
)
4950
from google.cloud.spanner_dbapi.transaction_helper import CursorStatementType
5051
from google.cloud.spanner_dbapi.utils import PeekIterator
@@ -272,6 +273,17 @@ def _execute(self, sql, args=None, call_from_execute_many=False):
272273
self._batch_DDLs(sql)
273274
if not self.connection._client_transaction_started:
274275
self.connection.run_prior_DDL_statements()
276+
elif (
277+
self.connection.autocommit_dml_mode
278+
is AutocommitDmlMode.PARTITIONED_NON_ATOMIC
279+
):
280+
self._row_count = self.connection.database.execute_partitioned_dml(
281+
sql,
282+
params=args,
283+
param_types=self._parsed_statement.statement.param_types,
284+
request_options=self.connection.request_options,
285+
)
286+
self._result_set = None
275287
else:
276288
self._execute_in_rw_transaction()
277289

google/cloud/spanner_dbapi/parsed_statement.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ class ClientSideStatementType(Enum):
3636
PARTITION_QUERY = 9
3737
RUN_PARTITION = 10
3838
RUN_PARTITIONED_QUERY = 11
39+
SET_AUTOCOMMIT_DML_MODE = 12
40+
41+
42+
class AutocommitDmlMode(Enum):
43+
TRANSACTIONAL = 1
44+
PARTITIONED_NON_ATOMIC = 2
3945

4046

4147
@dataclass

tests/system/test_dbapi.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
OperationalError,
2727
RetryAborted,
2828
)
29+
from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode
2930
from google.cloud.spanner_v1 import JsonObject
3031
from google.cloud.spanner_v1 import gapic_version as package_version
3132
from google.api_core.datetime_helpers import DatetimeWithNanoseconds
@@ -669,6 +670,27 @@ def test_run_partitioned_query(self):
669670
assert len(rows) == 10
670671
self._conn.commit()
671672

673+
def test_partitioned_dml_query(self):
674+
"""Test partitioned_dml query works in autocommit mode."""
675+
self._cursor.execute("start batch dml")
676+
for i in range(1, 11):
677+
self._insert_row(i)
678+
self._cursor.execute("run batch")
679+
self._conn.commit()
680+
681+
self._conn.autocommit = True
682+
self._cursor.execute("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC")
683+
self._cursor.execute("DELETE FROM contacts WHERE contact_id > 3")
684+
assert self._cursor.rowcount == 7
685+
686+
self._cursor.execute("set autocommit_dml_mode = TRANSACTIONAL")
687+
assert self._conn.autocommit_dml_mode == AutocommitDmlMode.TRANSACTIONAL
688+
689+
self._conn.autocommit = False
690+
# Test changing autocommit_dml_mode is not allowed when connection is in autocommit mode
691+
with pytest.raises(ProgrammingError):
692+
self._cursor.execute("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC")
693+
672694
def _insert_row(self, i):
673695
self._cursor.execute(
674696
f"""

tests/unit/spanner_dbapi/test_connection.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
ParsedStatement,
3434
StatementType,
3535
Statement,
36+
ClientSideStatementType,
37+
AutocommitDmlMode,
3638
)
3739

3840
PROJECT = "test-project"
@@ -433,6 +435,62 @@ def test_abort_dml_batch(self, mock_batch_dml_executor):
433435
self.assertEqual(self._under_test._batch_mode, BatchMode.NONE)
434436
self.assertEqual(self._under_test._batch_dml_executor, None)
435437

438+
def test_set_autocommit_dml_mode_with_autocommit_false(self):
439+
self._under_test.autocommit = False
440+
parsed_statement = ParsedStatement(
441+
StatementType.CLIENT_SIDE,
442+
Statement("sql"),
443+
ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE,
444+
["PARTITIONED_NON_ATOMIC"],
445+
)
446+
447+
with self.assertRaises(ProgrammingError):
448+
self._under_test._set_autocommit_dml_mode(parsed_statement)
449+
450+
def test_set_autocommit_dml_mode_with_readonly(self):
451+
self._under_test.autocommit = True
452+
self._under_test.read_only = True
453+
parsed_statement = ParsedStatement(
454+
StatementType.CLIENT_SIDE,
455+
Statement("sql"),
456+
ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE,
457+
["PARTITIONED_NON_ATOMIC"],
458+
)
459+
460+
with self.assertRaises(ProgrammingError):
461+
self._under_test._set_autocommit_dml_mode(parsed_statement)
462+
463+
def test_set_autocommit_dml_mode_with_batch_mode(self):
464+
self._under_test.autocommit = True
465+
parsed_statement = ParsedStatement(
466+
StatementType.CLIENT_SIDE,
467+
Statement("sql"),
468+
ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE,
469+
["PARTITIONED_NON_ATOMIC"],
470+
)
471+
472+
self._under_test._set_autocommit_dml_mode(parsed_statement)
473+
474+
assert (
475+
self._under_test.autocommit_dml_mode
476+
== AutocommitDmlMode.PARTITIONED_NON_ATOMIC
477+
)
478+
479+
def test_set_autocommit_dml_mode(self):
480+
self._under_test.autocommit = True
481+
parsed_statement = ParsedStatement(
482+
StatementType.CLIENT_SIDE,
483+
Statement("sql"),
484+
ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE,
485+
["PARTITIONED_NON_ATOMIC"],
486+
)
487+
488+
self._under_test._set_autocommit_dml_mode(parsed_statement)
489+
assert (
490+
self._under_test.autocommit_dml_mode
491+
== AutocommitDmlMode.PARTITIONED_NON_ATOMIC
492+
)
493+
436494
@mock.patch("google.cloud.spanner_v1.database.Database", autospec=True)
437495
def test_run_prior_DDL_statements(self, mock_database):
438496
from google.cloud.spanner_dbapi import Connection, InterfaceError

tests/unit/spanner_dbapi/test_parse_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,20 @@ def test_run_partitioned_query_classify_stmt(self):
115115
),
116116
)
117117

118+
def test_set_autocommit_dml_mode_stmt(self):
119+
parsed_statement = classify_statement(
120+
" set autocommit_dml_mode = PARTITIONED_NON_ATOMIC "
121+
)
122+
self.assertEqual(
123+
parsed_statement,
124+
ParsedStatement(
125+
StatementType.CLIENT_SIDE,
126+
Statement("set autocommit_dml_mode = PARTITIONED_NON_ATOMIC"),
127+
ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE,
128+
["PARTITIONED_NON_ATOMIC"],
129+
),
130+
)
131+
118132
@unittest.skipIf(skip_condition, skip_message)
119133
def test_sql_pyformat_args_to_spanner(self):
120134
from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner

0 commit comments

Comments
 (0)