-
Notifications
You must be signed in to change notification settings - Fork 101
feat: Implementing client side statements in dbapi (starting with commit) #1037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bedb240
6955a6c
5233060
451d973
8d88ba4
08035a1
1674cba
4c4d2de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Copyright 2023 Google LLC All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| from google.cloud.spanner_dbapi.parsed_statement import ( | ||
| ParsedStatement, | ||
| ClientSideStatementType, | ||
| ) | ||
|
|
||
|
|
||
| def execute(connection, parsed_statement: ParsedStatement): | ||
| """Executes the client side statements by calling the relevant method. | ||
|
|
||
| It is an internal method that can make backwards-incompatible changes. | ||
|
|
||
| :type parsed_statement: ParsedStatement | ||
| :param parsed_statement: parsed_statement based on the sql query | ||
| """ | ||
| if parsed_statement.client_side_statement_type == ClientSideStatementType.COMMIT: | ||
| return connection.commit() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Copyright 2023 Google LLC All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import re | ||
|
|
||
| from google.cloud.spanner_dbapi.parsed_statement import ( | ||
| ParsedStatement, | ||
| StatementType, | ||
| ClientSideStatementType, | ||
| ) | ||
|
|
||
| RE_COMMIT = re.compile(r"^\s*(COMMIT)(TRANSACTION)?", re.IGNORECASE) | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def parse_stmt(query): | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Parses the sql query to check if it matches with any of the client side | ||
| statement regex. | ||
|
|
||
| It is an internal method that can make backwards-incompatible changes. | ||
|
|
||
| :type query: str | ||
| :param query: sql query | ||
|
|
||
| :rtype: ParsedStatement | ||
| :returns: ParsedStatement object. | ||
| """ | ||
| if RE_COMMIT.match(query): | ||
| return ParsedStatement( | ||
| StatementType.CLIENT_SIDE, query, ClientSideStatementType.COMMIT | ||
| ) | ||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,11 @@ | |
| import sqlparse | ||
| from google.cloud import spanner_v1 as spanner | ||
| from google.cloud.spanner_v1 import JsonObject | ||
| from . import client_side_statement_parser | ||
| from deprecated import deprecated | ||
|
|
||
| from .exceptions import Error | ||
| from .parsed_statement import ParsedStatement, StatementType | ||
| from .types import DateStr, TimestampStr | ||
| from .utils import sanitize_literals_for_upload | ||
|
|
||
|
|
@@ -174,12 +177,11 @@ | |
| RE_PYFORMAT = re.compile(r"(%s|%\([^\(\)]+\)s)+", re.DOTALL) | ||
|
|
||
|
|
||
| @deprecated(reason="This method is deprecated. Use _classify_stmt method") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this pulls in two new dependencies (deprecated, wrapt) into this library -- this can be replaced entirely with this inside the function body: warnings.warn("This method is deprecated. Use classify_statement method instead", stacklevel=2)this uses
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would one be open to a PR to do this instead?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ankiaga Would you mind taking a look what would be the best solution here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. threw one together: #1120 |
||
| def classify_stmt(query): | ||
| """Determine SQL query type. | ||
|
|
||
| :type query: str | ||
| :param query: A SQL query. | ||
|
|
||
| :rtype: str | ||
| :returns: The query type name. | ||
| """ | ||
|
|
@@ -203,6 +205,39 @@ def classify_stmt(query): | |
| return STMT_UPDATING | ||
|
|
||
|
|
||
| def classify_statement(query): | ||
| """Determine SQL query type. | ||
|
|
||
| It is an internal method that can make backwards-incompatible changes. | ||
|
|
||
| :type query: str | ||
| :param query: A SQL query. | ||
|
|
||
| :rtype: ParsedStatement | ||
| :returns: parsed statement attributes. | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| # sqlparse will strip Cloud Spanner comments, | ||
| # still, special commenting styles, like | ||
| # PostgreSQL dollar quoted comments are not | ||
| # supported and will not be stripped. | ||
| query = sqlparse.format(query, strip_comments=True).strip() | ||
| parsed_statement = client_side_statement_parser.parse_stmt(query) | ||
| if parsed_statement is not None: | ||
| return parsed_statement | ||
| if RE_DDL.match(query): | ||
| return ParsedStatement(StatementType.DDL, query) | ||
|
|
||
| if RE_IS_INSERT.match(query): | ||
| return ParsedStatement(StatementType.INSERT, query) | ||
|
|
||
| if RE_NON_UPDATE.match(query) or RE_WITH.match(query): | ||
| # As of 13-March-2020, Cloud Spanner only supports WITH for DQL | ||
| # statements and doesn't yet support WITH for DML statements. | ||
| return ParsedStatement(StatementType.QUERY, query) | ||
|
|
||
| return ParsedStatement(StatementType.UPDATE, query) | ||
|
|
||
|
|
||
| def sql_pyformat_args_to_spanner(sql, params): | ||
| """ | ||
| Transform pyformat set SQL to named arguments for Cloud Spanner. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Copyright 20203 Google LLC All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from dataclasses import dataclass | ||
| from enum import Enum | ||
|
|
||
|
|
||
| class StatementType(Enum): | ||
| CLIENT_SIDE = 1 | ||
| DDL = 2 | ||
| QUERY = 3 | ||
| UPDATE = 4 | ||
| INSERT = 5 | ||
|
|
||
|
|
||
| class ClientSideStatementType(Enum): | ||
| COMMIT = 1 | ||
| BEGIN = 2 | ||
|
|
||
|
|
||
| @dataclass | ||
| class ParsedStatement: | ||
| statement_type: StatementType | ||
| query: str | ||
| client_side_statement_type: ClientSideStatementType = None | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
|
|
||
| from google.cloud import spanner_v1 | ||
| from google.cloud._helpers import UTC | ||
|
|
||
| from google.cloud.spanner_dbapi import Cursor | ||
| from google.cloud.spanner_dbapi.connection import connect | ||
| from google.cloud.spanner_dbapi.connection import Connection | ||
| from google.cloud.spanner_dbapi.exceptions import ProgrammingError | ||
|
|
@@ -72,37 +74,11 @@ def dbapi_database(raw_database): | |
|
|
||
| def test_commit(shared_instance, dbapi_database): | ||
| """Test committing a transaction with several statements.""" | ||
| want_row = ( | ||
| 1, | ||
| "updated-first-name", | ||
| "last-name", | ||
| "[email protected]", | ||
| ) | ||
| # connect to the test database | ||
| conn = Connection(shared_instance, dbapi_database) | ||
| cursor = conn.cursor() | ||
|
|
||
| # execute several DML statements within one transaction | ||
| cursor.execute( | ||
| """ | ||
| INSERT INTO contacts (contact_id, first_name, last_name, email) | ||
| VALUES (1, 'first-name', 'last-name', '[email protected]') | ||
| """ | ||
| ) | ||
| cursor.execute( | ||
| """ | ||
| UPDATE contacts | ||
| SET first_name = 'updated-first-name' | ||
| WHERE first_name = 'first-name' | ||
| """ | ||
| ) | ||
| cursor.execute( | ||
| """ | ||
| UPDATE contacts | ||
| SET email = '[email protected]' | ||
| WHERE email = '[email protected]' | ||
| """ | ||
| ) | ||
| want_row = _execute_common_precommit_statements(cursor) | ||
| conn.commit() | ||
|
|
||
| # read the resulting data from the database | ||
|
|
@@ -116,6 +92,25 @@ def test_commit(shared_instance, dbapi_database): | |
| conn.close() | ||
|
|
||
|
|
||
| def test_commit_client_side(shared_instance, dbapi_database): | ||
| """Test committing a transaction with several statements.""" | ||
| # connect to the test database | ||
| conn = Connection(shared_instance, dbapi_database) | ||
| cursor = conn.cursor() | ||
|
|
||
| want_row = _execute_common_precommit_statements(cursor) | ||
| cursor.execute("""COMMIT""") | ||
|
|
||
| # read the resulting data from the database | ||
| cursor.execute("SELECT * FROM contacts") | ||
| got_rows = cursor.fetchall() | ||
| conn.commit() | ||
| cursor.close() | ||
| conn.close() | ||
|
|
||
| assert got_rows == [want_row] | ||
|
|
||
|
|
||
| def test_rollback(shared_instance, dbapi_database): | ||
| """Test rollbacking a transaction with several statements.""" | ||
| want_row = (2, "first-name", "last-name", "[email protected]") | ||
|
|
@@ -810,3 +805,33 @@ def test_dml_returning_delete(shared_instance, dbapi_database, autocommit): | |
| assert cur.fetchone() == (1, "first-name") | ||
| assert cur.rowcount == 1 | ||
| conn.commit() | ||
|
|
||
|
|
||
| def _execute_common_precommit_statements(cursor: Cursor): | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # execute several DML statements within one transaction | ||
| cursor.execute( | ||
| """ | ||
| INSERT INTO contacts (contact_id, first_name, last_name, email) | ||
ankiaga marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| VALUES (1, 'first-name', 'last-name', '[email protected]') | ||
| """ | ||
| ) | ||
| cursor.execute( | ||
| """ | ||
| UPDATE contacts | ||
| SET first_name = 'updated-first-name' | ||
| WHERE first_name = 'first-name' | ||
| """ | ||
| ) | ||
| cursor.execute( | ||
| """ | ||
| UPDATE contacts | ||
| SET email = '[email protected]' | ||
| WHERE email = '[email protected]' | ||
| """ | ||
| ) | ||
| return ( | ||
| 1, | ||
| "updated-first-name", | ||
| "last-name", | ||
| "[email protected]", | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.