diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml index 19f49c5e4b..d74aa0fa00 100644 --- a/.github/workflows/integration-tests-against-emulator.yaml +++ b/.github/workflows/integration-tests-against-emulator.yaml @@ -10,7 +10,7 @@ jobs: services: emulator: - image: gcr.io/cloud-spanner-emulator/emulator:1.5.37 + image: gcr.io/cloud-spanner-emulator/emulator ports: - 9010:9010 - 9020:9020 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dba3bd5369..5dc714bd36 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.56.0" + ".": "3.57.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d809fa0c1..a00f09f300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-cloud-spanner/#history +## [3.57.0](https://github.com/googleapis/python-spanner/compare/v3.56.0...v3.57.0) (2025-08-14) + + +### Features + +* Support configuring logger in dbapi kwargs ([#1400](https://github.com/googleapis/python-spanner/issues/1400)) ([ffa5c9e](https://github.com/googleapis/python-spanner/commit/ffa5c9e627583ab0635dcaa5512b6e034d811d86)) + ## [3.56.0](https://github.com/googleapis/python-spanner/compare/v3.55.0...v3.56.0) (2025-07-24) diff --git a/google/cloud/spanner_admin_database_v1/gapic_version.py b/google/cloud/spanner_admin_database_v1/gapic_version.py index 9f754a9a74..5c0faa7b3e 100644 --- a/google/cloud/spanner_admin_database_v1/gapic_version.py +++ b/google/cloud/spanner_admin_database_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.56.0" # {x-release-please-version} +__version__ = "3.57.0" # {x-release-please-version} diff --git a/google/cloud/spanner_admin_instance_v1/gapic_version.py b/google/cloud/spanner_admin_instance_v1/gapic_version.py index 9f754a9a74..5c0faa7b3e 100644 --- a/google/cloud/spanner_admin_instance_v1/gapic_version.py +++ b/google/cloud/spanner_admin_instance_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.56.0" # {x-release-please-version} +__version__ = "3.57.0" # {x-release-please-version} diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 1a2b117e4c..db18f44067 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -819,8 +819,9 @@ def connect( instance = client.instance(instance_id) database = None if database_id: + logger = kwargs.get("logger") database = instance.database( - database_id, pool=pool, database_role=database_role + database_id, pool=pool, database_role=database_role, logger=logger ) conn = Connection(instance, database, **kwargs) if pool is not None: diff --git a/google/cloud/spanner_v1/gapic_version.py b/google/cloud/spanner_v1/gapic_version.py index 9f754a9a74..5c0faa7b3e 100644 --- a/google/cloud/spanner_v1/gapic_version.py +++ b/google/cloud/spanner_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "3.56.0" # {x-release-please-version} +__version__ = "3.57.0" # {x-release-please-version} diff --git a/google/cloud/spanner_v1/snapshot.py b/google/cloud/spanner_v1/snapshot.py index 295222022b..5633cd4486 100644 --- a/google/cloud/spanner_v1/snapshot.py +++ b/google/cloud/spanner_v1/snapshot.py @@ -133,7 +133,11 @@ def _restart_on_unavailable( # Update the transaction from the response. if transaction is not None: transaction._update_for_result_set_pb(item) - if item.precommit_token is not None and transaction is not None: + if ( + item._pb is not None + and item._pb.HasField("precommit_token") + and transaction is not None + ): transaction._update_for_precommit_token_pb(item.precommit_token) if item.resume_token: @@ -1029,7 +1033,7 @@ def _update_for_transaction_pb(self, transaction_pb: Transaction) -> None: if self._transaction_id is None and transaction_pb.id: self._transaction_id = transaction_pb.id - if transaction_pb.precommit_token: + if transaction_pb._pb.HasField("precommit_token"): self._update_for_precommit_token_pb_unsafe(transaction_pb.precommit_token) def _update_for_precommit_token_pb( diff --git a/google/cloud/spanner_v1/transaction.py b/google/cloud/spanner_v1/transaction.py index 314c5d13a4..5db809f91c 100644 --- a/google/cloud/spanner_v1/transaction.py +++ b/google/cloud/spanner_v1/transaction.py @@ -328,14 +328,20 @@ def before_next_retry(nth_retry, delay_in_seconds): # successfully commit, and must be retried with the new precommit token. # The mutations should not be included in the new request, and no further # retries or exception handling should be performed. - if commit_response_pb.precommit_token: + if commit_response_pb._pb.HasField("precommit_token"): add_span_event(span, commit_retry_event_name) + nth_request = database._next_nth_request commit_response_pb = api.commit( request=CommitRequest( precommit_token=commit_response_pb.precommit_token, **common_commit_request_args, ), - metadata=metadata, + metadata=database.metadata_with_request_id( + nth_request, + 1, + metadata, + span, + ), ) add_span_event(span, "Commit Done") @@ -521,7 +527,7 @@ def wrapped_method(*args, **kwargs): if is_inline_begin: self._lock.release() - if result_set_pb.precommit_token is not None: + if result_set_pb._pb.HasField("precommit_token"): self._update_for_precommit_token_pb(result_set_pb.precommit_token) return result_set_pb.stats.row_count_exact diff --git a/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json index f0d4300339..e6f99e7e7d 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.admin.database.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner-admin-database", - "version": "3.56.0" + "version": "3.57.0" }, "snippets": [ { diff --git a/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json index b847191deb..af6c65815a 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.admin.instance.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner-admin-instance", - "version": "3.56.0" + "version": "3.57.0" }, "snippets": [ { diff --git a/samples/generated_samples/snippet_metadata_google.spanner.v1.json b/samples/generated_samples/snippet_metadata_google.spanner.v1.json index 9bf7db31cc..0c303b9ff0 100644 --- a/samples/generated_samples/snippet_metadata_google.spanner.v1.json +++ b/samples/generated_samples/snippet_metadata_google.spanner.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-spanner", - "version": "3.56.0" + "version": "3.57.0" }, "snippets": [ { diff --git a/tests/mockserver_tests/mock_server_test_base.py b/tests/mockserver_tests/mock_server_test_base.py index 443b75ada7..117b649e1b 100644 --- a/tests/mockserver_tests/mock_server_test_base.py +++ b/tests/mockserver_tests/mock_server_test_base.py @@ -11,7 +11,7 @@ # 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 logging import unittest import grpc @@ -170,12 +170,15 @@ class MockServerTestBase(unittest.TestCase): spanner_service: SpannerServicer = None database_admin_service: DatabaseAdminServicer = None port: int = None + logger: logging.Logger = None def __init__(self, *args, **kwargs): super(MockServerTestBase, self).__init__(*args, **kwargs) self._client = None self._instance = None self._database = None + self.logger = logging.getLogger("MockServerTestBase") + self.logger.setLevel(logging.WARN) @classmethod def setup_class(cls): @@ -227,6 +230,7 @@ def database(self) -> Database: "test-database", pool=FixedSizePool(size=10), enable_interceptors_in_tests=True, + logger=self.logger, ) return self._database diff --git a/tests/mockserver_tests/test_request_id_header.py b/tests/mockserver_tests/test_request_id_header.py index 413e0f6514..055d9d97b5 100644 --- a/tests/mockserver_tests/test_request_id_header.py +++ b/tests/mockserver_tests/test_request_id_header.py @@ -227,10 +227,6 @@ def test_database_execute_partitioned_dml_request_id(self): (1, REQ_RAND_PROCESS_ID, NTH_CLIENT, CHANNEL_ID, exec_sql_seq, 1), ) ] - print(f"Filtered unary segments: {filtered_unary_segments}") - print(f"Want unary segments: {want_unary_segments}") - print(f"Got stream segments: {got_stream_segments}") - print(f"Want stream segments: {want_stream_segments}") assert all(seg in filtered_unary_segments for seg in want_unary_segments) assert got_stream_segments == want_stream_segments @@ -269,8 +265,6 @@ def test_unary_retryable_error(self): (1, REQ_RAND_PROCESS_ID, NTH_CLIENT, CHANNEL_ID, exec_sql_seq, 1), ) ] - print(f"Got stream segments: {got_stream_segments}") - print(f"Want stream segments: {want_stream_segments}") assert got_stream_segments == want_stream_segments def test_streaming_retryable_error(self): diff --git a/tests/system/test_database_api.py b/tests/system/test_database_api.py index 57ce49c8a2..e3c18ece10 100644 --- a/tests/system/test_database_api.py +++ b/tests/system/test_database_api.py @@ -569,7 +569,10 @@ def test_db_run_in_transaction_then_snapshot_execute_sql(shared_database): batch.delete(sd.TABLE, sd.ALL) def _unit_of_work(transaction, test): - rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL)) + # TODO: Remove query and execute a read instead when the Emulator has been fixed + # and returns pre-commit tokens for streaming read results. + rows = list(transaction.execute_sql(sd.SQL)) + # rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL)) assert rows == [] transaction.insert_or_update(test.TABLE, test.COLUMNS, test.ROW_DATA) @@ -882,7 +885,10 @@ def test_db_run_in_transaction_w_max_commit_delay(shared_database): batch.delete(sd.TABLE, sd.ALL) def _unit_of_work(transaction, test): - rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL)) + # TODO: Remove query and execute a read instead when the Emulator has been fixed + # and returns pre-commit tokens for streaming read results. + rows = list(transaction.execute_sql(sd.SQL)) + # rows = list(transaction.read(test.TABLE, test.COLUMNS, sd.ALL)) assert rows == [] transaction.insert_or_update(test.TABLE, test.COLUMNS, test.ROW_DATA) diff --git a/tests/system/test_session_api.py b/tests/system/test_session_api.py index 4da4e2e0d1..04d8ad799a 100644 --- a/tests/system/test_session_api.py +++ b/tests/system/test_session_api.py @@ -932,6 +932,8 @@ def _transaction_read_then_raise(transaction): def test_transaction_read_and_insert_or_update_then_commit( sessions_database, sessions_to_delete, + # TODO: Re-enable when the emulator returns pre-commit tokens for reads. + not_emulator, ): # [START spanner_test_dml_read_your_writes] sd = _sample_data @@ -1586,7 +1588,11 @@ def _read_w_concurrent_update(transaction, pkey): transaction.update(COUNTERS_TABLE, COUNTERS_COLUMNS, [[pkey, value + 1]]) -def test_transaction_read_w_concurrent_updates(sessions_database): +def test_transaction_read_w_concurrent_updates( + sessions_database, + # TODO: Re-enable when the Emulator returns pre-commit tokens for streaming reads. + not_emulator, +): pkey = "read_w_concurrent_updates" _transaction_concurrency_helper(sessions_database, _read_w_concurrent_update, pkey) diff --git a/tests/unit/spanner_dbapi/test_connect.py b/tests/unit/spanner_dbapi/test_connect.py index 7f4fb4c7f3..5fd2b74a17 100644 --- a/tests/unit/spanner_dbapi/test_connect.py +++ b/tests/unit/spanner_dbapi/test_connect.py @@ -59,7 +59,7 @@ def test_w_implicit(self, mock_client): self.assertIs(connection.database, database) instance.database.assert_called_once_with( - DATABASE, pool=None, database_role=None + DATABASE, pool=None, database_role=None, logger=None ) # Database constructs its own pool self.assertIsNotNone(connection.database._pool) @@ -107,7 +107,7 @@ def test_w_explicit(self, mock_client): self.assertIs(connection.database, database) instance.database.assert_called_once_with( - DATABASE, pool=pool, database_role=role + DATABASE, pool=pool, database_role=role, logger=None ) def test_w_credential_file_path(self, mock_client): diff --git a/tests/unit/spanner_dbapi/test_connection.py b/tests/unit/spanner_dbapi/test_connection.py index 0bfab5bab9..6e8159425f 100644 --- a/tests/unit/spanner_dbapi/test_connection.py +++ b/tests/unit/spanner_dbapi/test_connection.py @@ -888,8 +888,9 @@ def database( pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, database_role=None, + logger=None, ): - return _Database(database_id, pool, database_dialect, database_role) + return _Database(database_id, pool, database_dialect, database_role, logger) class _Database(object): @@ -899,8 +900,10 @@ def __init__( pool=None, database_dialect=DatabaseDialect.GOOGLE_STANDARD_SQL, database_role=None, + logger=None, ): self.name = database_id self.pool = pool self.database_dialect = database_dialect self.database_role = database_role + self.logger = logger diff --git a/tests/unit/test_snapshot.py b/tests/unit/test_snapshot.py index e7cfce3761..5e60d71bd6 100644 --- a/tests/unit/test_snapshot.py +++ b/tests/unit/test_snapshot.py @@ -158,6 +158,7 @@ def _make_item(self, value, resume_token=b"", metadata=None): resume_token=resume_token, metadata=metadata, precommit_token=None, + _pb=None, spec=["value", "resume_token", "metadata", "precommit_token"], ) diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index 05bb25de6b..7a33372dae 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -533,7 +533,7 @@ def _commit_helper( ) commit.assert_any_call( request=expected_retry_request, - metadata=base_metadata, + metadata=expected_retry_metadata, ) if not HAS_OPENTELEMETRY_INSTALLED: