diff --git a/.librarian/generator-input/librarian.py b/.librarian/generator-input/librarian.py index 46c2e8dbb4..e458f64615 100644 --- a/.librarian/generator-input/librarian.py +++ b/.librarian/generator-input/librarian.py @@ -250,4 +250,4 @@ # Use a python runtime which is available in the owlbot post processor here # https://github.com/googleapis/synthtool/blob/master/docker/owlbot/python/Dockerfile -s.shell.run(["nox", "-s", "blacken-3.14"], hide_output=False) +s.shell.run(["nox", "-s", "format-3.14"], hide_output=False) diff --git a/.librarian/generator-input/noxfile.py b/.librarian/generator-input/noxfile.py index 82715de072..cdcdcd9f6f 100644 --- a/.librarian/generator-input/noxfile.py +++ b/.librarian/generator-input/noxfile.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + # Generated by synthtool. DO NOT EDIT! from __future__ import absolute_import @@ -64,6 +65,7 @@ SYSTEM_TEST_STANDARD_DEPENDENCIES: List[str] = [ "mock", "pytest", + "pytest-asyncio", "google-cloud-testutils", ] SYSTEM_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] @@ -217,9 +219,8 @@ def unit(session, protobuf_implementation): session.install("protobuf<4") # Run py.test against the unit tests. - session.run( + args = [ "py.test", - "--quiet", "-s", f"--junitxml=unit_{session.python}_sponge_log.xml", "--cov=google", @@ -228,8 +229,13 @@ def unit(session, protobuf_implementation): "--cov-config=.coveragerc", "--cov-report=", "--cov-fail-under=0", - os.path.join("tests", "unit"), - *session.posargs, + ] + if not session.posargs: + args.append(os.path.join("tests", "unit")) + args.extend(session.posargs) + + session.run( + *args, env={ "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, }, @@ -362,12 +368,19 @@ def system(session, protobuf_implementation, database_dialect): # Run py.test against the system tests. if system_test_exists: - session.run( + args = [ "py.test", "--quiet", + "-o", + "asyncio_mode=auto", f"--junitxml=system_{session.python}_sponge_log.xml", - system_test_path, - *session.posargs, + ] + if not session.posargs: + args.append(system_test_path) + args.extend(session.posargs) + + session.run( + *args, env={ "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, "SPANNER_DATABASE_DIALECT": database_dialect, @@ -375,12 +388,47 @@ def system(session, protobuf_implementation, database_dialect): }, ) elif system_test_folder_exists: + # Run sync tests + sync_args = [ + "py.test", + "--quiet", + "-o", + "asyncio_mode=auto", + f"--junitxml=system_{session.python}_sync_sponge_log.xml", + ] + if not session.posargs: + sync_args.append(os.path.join("tests", "system")) + sync_args.append("--ignore=tests/system/_async") + else: + sync_args.extend(session.posargs) + session.run( + *sync_args, + env={ + "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, + "SPANNER_DATABASE_DIALECT": database_dialect, + "SKIP_BACKUP_TESTS": "true", + }, + ) + + # Run async tests + async_args = [ "py.test", "--quiet", - f"--junitxml=system_{session.python}_sponge_log.xml", - system_test_folder_path, - *session.posargs, + "-o", + "asyncio_mode=auto", + f"--junitxml=system_{session.python}_async_sponge_log.xml", + ] + if not session.posargs: + async_args.append(os.path.join("tests", "system", "_async")) + else: + # If posargs are provided, only run if they match async tests + # or just skip if they were already run in sync. + # For simplicity, we only run async folder if no posargs. + return + + session.run( + *async_args, env={ "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, "SPANNER_DATABASE_DIALECT": database_dialect, @@ -551,6 +599,10 @@ def prerelease_deps(session, protobuf_implementation, database_dialect): "google-cloud-testutils", # dependencies of google-cloud-testutils" "click", + # dependency of google-auth + "cffi", + "cryptography", + "cachetools", ] for dep in prerel_deps: @@ -589,6 +641,8 @@ def prerelease_deps(session, protobuf_implementation, database_dialect): session.run( "py.test", "--verbose", + "-o", + "asyncio_mode=auto", f"--junitxml=system_{session.python}_sponge_log.xml", system_test_path, *session.posargs, @@ -602,6 +656,8 @@ def prerelease_deps(session, protobuf_implementation, database_dialect): session.run( "py.test", "--verbose", + "-o", + "asyncio_mode=auto", f"--junitxml=system_{session.python}_sponge_log.xml", system_test_folder_path, *session.posargs, @@ -611,3 +667,10 @@ def prerelease_deps(session, protobuf_implementation, database_dialect): "SKIP_BACKUP_TESTS": "true", }, ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def generate(session): + """Regenerate synchronous code from asynchronous code.""" + session.install("black", "autoflake") + session.run("python", ".cross_sync/generate.py", "google/cloud/spanner_v1") diff --git a/.librarian/state.yaml b/.librarian/state.yaml index bc132b9050..315a9511e8 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -2,7 +2,7 @@ image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-li libraries: - id: google-cloud-spanner version: 3.63.0 - last_generated_commit: a17b84add8318f780fcc8a027815d5fee644b9f7 + last_generated_commit: 94bc991b3f1df58bc841801631715894a7ac5e28 apis: - path: google/spanner/admin/instance/v1 service_config: spanner.yaml @@ -21,7 +21,8 @@ libraries: - ^google/cloud/spanner_v1/types - ^google/cloud/spanner_admin_database_v1 - ^google/cloud/spanner_admin_instance_v1 - - ^tests/unit/gapic + - ^tests/unit/gapic/spanner + - ^tests/unit/gapic/__init__.py - ^tests/__init__.py - ^tests/unit/__init__.py - ^.pre-commit-config.yaml diff --git a/google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py b/google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py index 4bf78b0ba7..1df4301475 100644 --- a/google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +++ b/google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py @@ -542,11 +542,24 @@ class AutoscalingTargets(proto.Message): Attributes: high_priority_cpu_utilization_percent (int): - Required. The target high priority cpu utilization + Optional. The target high priority cpu utilization percentage that the autoscaler should be trying to achieve for the instance. This number is on a scale from 0 (no utilization) to 100 (full utilization). The valid range is - [10, 90] inclusive. + [10, 90] inclusive. If not specified or set to 0, the + autoscaler skips scaling based on high priority CPU + utilization. + total_cpu_utilization_percent (int): + Optional. The target total CPU utilization percentage that + the autoscaler should be trying to achieve for the instance. + This number is on a scale from 0 (no utilization) to 100 + (full utilization). The valid range is [10, 90] inclusive. + If not specified or set to 0, the autoscaler skips scaling + based on total CPU utilization. If both + ``high_priority_cpu_utilization_percent`` and + ``total_cpu_utilization_percent`` are specified, the + autoscaler provisions the larger of the two required compute + capacities to satisfy both targets. storage_utilization_percent (int): Required. The target storage utilization percentage that the autoscaler should be trying to achieve for the instance. @@ -558,6 +571,10 @@ class AutoscalingTargets(proto.Message): proto.INT32, number=1, ) + total_cpu_utilization_percent: int = proto.Field( + proto.INT32, + number=4, + ) storage_utilization_percent: int = proto.Field( proto.INT32, number=2, @@ -593,6 +610,58 @@ class AutoscalingConfigOverrides(proto.Message): Optional. If specified, overrides the autoscaling target high_priority_cpu_utilization_percent in the top-level autoscaling configuration for the selected replicas. + autoscaling_target_total_cpu_utilization_percent (int): + Optional. If specified, overrides the autoscaling target + ``total_cpu_utilization_percent`` in the top-level + autoscaling configuration for the selected replicas. + disable_high_priority_cpu_autoscaling (bool): + Optional. If true, disables high priority CPU autoscaling + for the selected replicas and ignores + [high_priority_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AutoscalingTargets.high_priority_cpu_utilization_percent] + in the top-level autoscaling configuration. + + When setting this field to true, setting + [autoscaling_target_high_priority_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.autoscaling_target_high_priority_cpu_utilization_percent] + field to a non-zero value for the same replica is not + supported. + + If false, the + [autoscaling_target_high_priority_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.autoscaling_target_high_priority_cpu_utilization_percent] + field in the replica will be used if set to a non-zero + value. Otherwise, the + [high_priority_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AutoscalingTargets.high_priority_cpu_utilization_percent] + field in the top-level autoscaling configuration will be + used. + + Setting both + [disable_high_priority_cpu_autoscaling][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.disable_high_priority_cpu_autoscaling] + and + [disable_total_cpu_autoscaling][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.disable_total_cpu_autoscaling] + to true for the same replica is not supported. + disable_total_cpu_autoscaling (bool): + Optional. If true, disables total CPU autoscaling for the + selected replicas and ignores + [total_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AutoscalingTargets.total_cpu_utilization_percent] + in the top-level autoscaling configuration. + + When setting this field to true, setting + [autoscaling_target_total_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.autoscaling_target_total_cpu_utilization_percent] + field to a non-zero value for the same replica is not + supported. + + If false, the + [autoscaling_target_total_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.autoscaling_target_total_cpu_utilization_percent] + field in the replica will be used if set to a non-zero + value. Otherwise, the + [total_cpu_utilization_percent][google.spanner.admin.instance.v1.AutoscalingConfig.AutoscalingTargets.total_cpu_utilization_percent] + field in the top-level autoscaling configuration will be + used. + + Setting both + [disable_high_priority_cpu_autoscaling][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.disable_high_priority_cpu_autoscaling] + and + [disable_total_cpu_autoscaling][google.spanner.admin.instance.v1.AutoscalingConfig.AsymmetricAutoscalingOption.AutoscalingConfigOverrides.disable_total_cpu_autoscaling] + to true for the same replica is not supported. """ autoscaling_limits: "AutoscalingConfig.AutoscalingLimits" = proto.Field( @@ -604,6 +673,18 @@ class AutoscalingConfigOverrides(proto.Message): proto.INT32, number=2, ) + autoscaling_target_total_cpu_utilization_percent: int = proto.Field( + proto.INT32, + number=4, + ) + disable_high_priority_cpu_autoscaling: bool = proto.Field( + proto.BOOL, + number=5, + ) + disable_total_cpu_autoscaling: bool = proto.Field( + proto.BOOL, + number=6, + ) replica_selection: common.ReplicaSelection = proto.Field( proto.MESSAGE, diff --git a/google/cloud/spanner_v1/__init__.py b/google/cloud/spanner_v1/__init__.py index c1e29e5576..ed7a32de70 100644 --- a/google/cloud/spanner_v1/__init__.py +++ b/google/cloud/spanner_v1/__init__.py @@ -63,7 +63,6 @@ BatchWriteRequest, BatchWriteResponse, BeginTransactionRequest, - ClientContext, CommitRequest, CreateSessionRequest, DeleteSessionRequest, @@ -124,7 +123,6 @@ "BatchWriteRequest", "BatchWriteResponse", "BeginTransactionRequest", - "ClientContext", "CommitRequest", "CommitResponse", "CreateSessionRequest", diff --git a/google/cloud/spanner_v1/_helpers.py b/google/cloud/spanner_v1/_helpers.py index a2772d434a..667b88bc05 100644 --- a/google/cloud/spanner_v1/_helpers.py +++ b/google/cloud/spanner_v1/_helpers.py @@ -32,7 +32,6 @@ from google.rpc.error_details_pb2 import RetryInfo from google.cloud._helpers import _date_from_iso8601_date -from google.cloud.spanner_v1.types import ClientContext from google.cloud.spanner_v1.types import RequestOptions from google.cloud.spanner_v1.data_types import JsonObject, Interval from google.cloud.spanner_v1.exceptions import wrap_with_request_id @@ -196,17 +195,17 @@ def _merge_query_options(base, merge): def _merge_client_context(base, merge): """Merge higher precedence ClientContext with current ClientContext. - :type base: :class:`~google.cloud.spanner_v1.types.ClientContext` + :type base: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` or :class:`dict` or None :param base: The current ClientContext that is intended for use. - :type merge: :class:`~google.cloud.spanner_v1.types.ClientContext` + :type merge: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` or :class:`dict` or None :param merge: The ClientContext that has a higher priority than base. These options should overwrite the fields in base. - :rtype: :class:`~google.cloud.spanner_v1.types.ClientContext` + :rtype: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` or None :returns: ClientContext object formed by merging the two given ClientContexts. @@ -215,15 +214,23 @@ def _merge_client_context(base, merge): return None # Avoid in-place modification of base - combined_pb = ClientContext()._pb + combined_pb = RequestOptions.ClientContext()._pb if base: - base_pb = ClientContext(base)._pb if isinstance(base, dict) else base._pb + base_pb = ( + RequestOptions.ClientContext(base)._pb + if isinstance(base, dict) + else base._pb + ) combined_pb.MergeFrom(base_pb) if merge: - merge_pb = ClientContext(merge)._pb if isinstance(merge, dict) else merge._pb + merge_pb = ( + RequestOptions.ClientContext(merge)._pb + if isinstance(merge, dict) + else merge._pb + ) combined_pb.MergeFrom(merge_pb) - combined = ClientContext(combined_pb) + combined = RequestOptions.ClientContext(combined_pb) if not combined.secure_context: return None @@ -233,18 +240,18 @@ def _merge_client_context(base, merge): def _validate_client_context(client_context): """Validate and convert client_context. - :type client_context: :class:`~google.cloud.spanner_v1.types.ClientContext` + :type client_context: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` or :class:`dict` :param client_context: (Optional) Client context to use. - :rtype: :class:`~google.cloud.spanner_v1.types.ClientContext` + :rtype: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` :returns: Validated ClientContext object or None. :raises TypeError: if client_context is not a ClientContext or a dict. """ if client_context is not None: if isinstance(client_context, dict): - client_context = ClientContext(client_context) - elif not isinstance(client_context, ClientContext): + client_context = RequestOptions.ClientContext(client_context) + elif not isinstance(client_context, RequestOptions.ClientContext): raise TypeError("client_context must be a ClientContext or a dict") return client_context @@ -256,7 +263,7 @@ def _merge_request_options(request_options, client_context): or :class:`dict` or None :param request_options: The current RequestOptions that is intended for use. - :type client_context: :class:`~google.cloud.spanner_v1.types.ClientContext` + :type client_context: :class:`~google.cloud.spanner_v1.types.RequestOptions.ClientContext` or :class:`dict` or None :param client_context: The ClientContext to merge into request_options. diff --git a/google/cloud/spanner_v1/types/__init__.py b/google/cloud/spanner_v1/types/__init__.py index 31c232c84f..059003b78f 100644 --- a/google/cloud/spanner_v1/types/__init__.py +++ b/google/cloud/spanner_v1/types/__init__.py @@ -34,7 +34,6 @@ BatchWriteRequest, BatchWriteResponse, BeginTransactionRequest, - ClientContext, CommitRequest, CreateSessionRequest, DeleteSessionRequest, @@ -88,7 +87,6 @@ "BatchWriteRequest", "BatchWriteResponse", "BeginTransactionRequest", - "ClientContext", "CommitRequest", "CreateSessionRequest", "DeleteSessionRequest", diff --git a/google/cloud/spanner_v1/types/commit_response.py b/google/cloud/spanner_v1/types/commit_response.py index aa189bf11b..a09997901a 100644 --- a/google/cloud/spanner_v1/types/commit_response.py +++ b/google/cloud/spanner_v1/types/commit_response.py @@ -20,7 +20,7 @@ from google.protobuf import timestamp_pb2 # type: ignore import proto # type: ignore -from google.cloud.spanner_v1.types import transaction +from google.cloud.spanner_v1.types import location, transaction __protobuf__ = proto.module( package="google.spanner.v1", @@ -54,6 +54,14 @@ class CommitResponse(proto.Message): ``IsolationLevel.REPEATABLE_READ``, then the snapshot timestamp is the timestamp at which all reads in the transaction ran. This timestamp is never returned. + cache_update (google.cloud.spanner_v1.types.CacheUpdate): + Optional. A cache update expresses a set of changes the + client should incorporate into its location cache. The + client should discard the changes if they are older than the + data it already has. This data can be obtained in response + to requests that included a ``RoutingHint`` field, but may + also be obtained by explicit location-fetching RPCs which + may be added in the future. """ class CommitStats(proto.Message): @@ -98,6 +106,11 @@ class CommitStats(proto.Message): number=5, message=timestamp_pb2.Timestamp, ) + cache_update: location.CacheUpdate = proto.Field( + proto.MESSAGE, + number=6, + message=location.CacheUpdate, + ) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/spanner_v1/types/location.py b/google/cloud/spanner_v1/types/location.py index 689ae1685a..5c8a8f9454 100644 --- a/google/cloud/spanner_v1/types/location.py +++ b/google/cloud/spanner_v1/types/location.py @@ -111,10 +111,9 @@ class Tablet(proto.Message): this tablet -- either an IP address or DNS hostname and a port number. location (str): - Where this tablet is located. In the Spanner - managed service, this is the name of a region, - such as "us-central1". In Spanner Omni, this is - a previously created location. + Where this tablet is located. This is the + name of a Google Cloud region, such as + "us-central1". role (google.cloud.spanner_v1.types.Tablet.Role): The role of the tablet. incarnation (bytes): @@ -593,11 +592,9 @@ class RoutingHint(proto.Message): incarnations. The server may include a ``CacheUpdate`` with new locations for those tablets. client_location (str): - If present, the client's current location. In - the Spanner managed service, this should be the - name of a Google Cloud zone or region, such as - "us-central1". In Spanner Omni, this should - correspond to a previously created location. + If present, the client's current location. + This should be the name of a Google Cloud zone + or region, such as "us-central1". If absent, the client's location will be assumed to be the same as the location of the server the diff --git a/google/cloud/spanner_v1/types/result_set.py b/google/cloud/spanner_v1/types/result_set.py index d36db741a7..43d65158b6 100644 --- a/google/cloud/spanner_v1/types/result_set.py +++ b/google/cloud/spanner_v1/types/result_set.py @@ -68,6 +68,14 @@ class ResultSet(proto.Message): token with the highest sequence number from this transaction attempt to the [Commit][google.spanner.v1.Spanner.Commit] request for this transaction. + cache_update (google.cloud.spanner_v1.types.CacheUpdate): + Optional. A cache update expresses a set of changes the + client should incorporate into its location cache. The + client should discard the changes if they are older than the + data it already has. This data can be obtained in response + to requests that included a ``RoutingHint`` field, but may + also be obtained by explicit location-fetching RPCs which + may be added in the future. """ metadata: "ResultSetMetadata" = proto.Field( @@ -90,6 +98,11 @@ class ResultSet(proto.Message): number=5, message=gs_transaction.MultiplexedSessionPrecommitToken, ) + cache_update: location.CacheUpdate = proto.Field( + proto.MESSAGE, + number=6, + message=location.CacheUpdate, + ) class PartialResultSet(proto.Message): diff --git a/google/cloud/spanner_v1/types/spanner.py b/google/cloud/spanner_v1/types/spanner.py index 888728e2cd..6bf22726fc 100644 --- a/google/cloud/spanner_v1/types/spanner.py +++ b/google/cloud/spanner_v1/types/spanner.py @@ -41,7 +41,6 @@ "ListSessionsResponse", "DeleteSessionRequest", "RequestOptions", - "ClientContext", "DirectedReadOptions", "ExecuteSqlRequest", "ExecuteBatchDmlRequest", @@ -334,14 +333,20 @@ class RequestOptions(proto.Message): A tag used for statistics collection about this transaction. Both ``request_tag`` and ``transaction_tag`` can be specified for a read or query that belongs to a transaction. - The value of transaction_tag should be the same for all - requests belonging to the same transaction. If this request - doesn't belong to any transaction, ``transaction_tag`` is - ignored. Legal characters for ``transaction_tag`` values are - all printable characters (ASCII 32 - 126) and the length of - a ``transaction_tag`` is limited to 50 characters. Values - that exceed this limit are truncated. Any leading underscore - (\_) characters are removed from the string. + To enable tagging on a transaction, ``transaction_tag`` must + be set to the same value for all requests belonging to the + same transaction, including + [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction]. + If this request doesn't belong to any transaction, + ``transaction_tag`` is ignored. Legal characters for + ``transaction_tag`` values are all printable characters + (ASCII 32 - 126) and the length of a ``transaction_tag`` is + limited to 50 characters. Values that exceed this limit are + truncated. Any leading underscore (\_) characters are + removed from the string. + client_context (google.cloud.spanner_v1.types.RequestOptions.ClientContext): + Optional. Optional context that may be needed + for some requests. """ class Priority(proto.Enum): @@ -381,6 +386,25 @@ class Priority(proto.Enum): PRIORITY_MEDIUM = 2 PRIORITY_HIGH = 3 + class ClientContext(proto.Message): + r"""Container for various pieces of client-owned context attached + to a request. + + Attributes: + secure_context (MutableMapping[str, google.protobuf.struct_pb2.Value]): + Optional. Map of parameter name to value for this request. + These values will be returned by any SECURE_CONTEXT() calls + invoked by this request (e.g., by queries against + Parameterized Secure Views). + """ + + secure_context: MutableMapping[str, struct_pb2.Value] = proto.MapField( + proto.STRING, + proto.MESSAGE, + number=1, + message=struct_pb2.Value, + ) + priority: Priority = proto.Field( proto.ENUM, number=1, @@ -397,27 +421,7 @@ class Priority(proto.Enum): client_context: ClientContext = proto.Field( proto.MESSAGE, number=4, - message="ClientContext", - ) - - -class ClientContext(proto.Message): - r"""Container for various pieces of client-owned context - attached to a request. - - Attributes: - secure_context (MutableMapping[str, google.protobuf.struct_pb2.Value]): - Optional. Map of parameter name to value for this request. - These values will be returned by any SECURE_CONTEXT() calls - invoked by this request (e.g., by queries against - Parameterized Secure Views). - """ - - secure_context: MutableMapping[str, struct_pb2.Value] = proto.MapField( - proto.STRING, - proto.MESSAGE, - number=1, - message=struct_pb2.Value, + message=ClientContext, ) @@ -686,8 +690,8 @@ class ExecuteSqlRequest(proto.Message): successful execution of a DML statement shouldn't be assumed until a subsequent ``Commit`` call completes successfully. routing_hint (google.cloud.spanner_v1.types.RoutingHint): - Optional. If present, it makes the Spanner - requests location-aware. + Optional. Makes the Spanner requests + location-aware if present. It gives the server hints that can be used to route the request to an appropriate server, potentially significantly decreasing latency and @@ -1143,8 +1147,8 @@ class PartitionQueryRequest(proto.Message): with a ``PartitionedDml`` transaction for large, partition-friendly DML operations. params (google.protobuf.struct_pb2.Struct): - Parameter names and values that bind to placeholders in the - SQL string. + Optional. Parameter names and values that bind to + placeholders in the SQL string. A parameter placeholder consists of the ``@`` character followed by the parameter name (for example, @@ -1160,9 +1164,10 @@ class PartitionQueryRequest(proto.Message): It's an error to execute a SQL statement with unbound parameters. param_types (MutableMapping[str, google.cloud.spanner_v1.types.Type]): - It isn't always possible for Cloud Spanner to infer the - right SQL type from a JSON value. For example, values of - type ``BYTES`` and values of type ``STRING`` both appear in + Optional. It isn't always possible for Cloud Spanner to + infer the right SQL type from a JSON value. For example, + values of type ``BYTES`` and values of type ``STRING`` both + appear in [params][google.spanner.v1.PartitionQueryRequest.params] as JSON strings. @@ -1425,8 +1430,8 @@ class ReadRequest(proto.Message): Optional. Lock Hint for the request, it can only be used with read-write transactions. routing_hint (google.cloud.spanner_v1.types.RoutingHint): - Optional. If present, it makes the Spanner - requests location-aware. + Optional. Makes the Spanner requests + location-aware if present. It gives the server hints that can be used to route the request to an appropriate server, potentially significantly decreasing latency and @@ -1608,6 +1613,15 @@ class BeginTransactionRequest(proto.Message): queries. You must randomly select one of the mutations from the mutation set and send it as a part of this request. + routing_hint (google.cloud.spanner_v1.types.RoutingHint): + Optional. Makes the Spanner requests + location-aware if present. + It gives the server hints that can be used to + route the request to an appropriate server, + potentially significantly decreasing latency and + improving throughput. To achieve improved + performance, most fields must be filled in with + accurate values. """ session: str = proto.Field( @@ -1629,6 +1643,11 @@ class BeginTransactionRequest(proto.Message): number=4, message=mutation.Mutation, ) + routing_hint: gs_location.RoutingHint = proto.Field( + proto.MESSAGE, + number=5, + message=gs_location.RoutingHint, + ) class CommitRequest(proto.Message): @@ -1687,6 +1706,15 @@ class CommitRequest(proto.Message): token with the highest sequence number received in this transaction attempt. Failing to do so results in a ``FailedPrecondition`` error. + routing_hint (google.cloud.spanner_v1.types.RoutingHint): + Optional. Makes the Spanner requests + location-aware if present. + It gives the server hints that can be used to + route the request to an appropriate server, + potentially significantly decreasing latency and + improving throughput. To achieve improved + performance, most fields must be filled in with + accurate values. """ session: str = proto.Field( @@ -1728,6 +1756,11 @@ class CommitRequest(proto.Message): number=9, message=gs_transaction.MultiplexedSessionPrecommitToken, ) + routing_hint: gs_location.RoutingHint = proto.Field( + proto.MESSAGE, + number=10, + message=gs_location.RoutingHint, + ) class RollbackRequest(proto.Message): @@ -1821,7 +1854,13 @@ class BatchWriteResponse(proto.Message): indicates a failure. commit_timestamp (google.protobuf.timestamp_pb2.Timestamp): The commit timestamp of the transaction that applied this - batch. Present if ``status`` is ``OK``, absent otherwise. + batch. Present if status is OK and the mutation groups were + applied, absent otherwise. + + For mutation groups with conditions, a status=OK and missing + commit_timestamp means that the mutation groups were not + applied due to the condition not being satisfied after + evaluation. """ indexes: MutableSequence[int] = proto.RepeatedField( diff --git a/google/cloud/spanner_v1/types/transaction.py b/google/cloud/spanner_v1/types/transaction.py index fd5cede050..f4764adbff 100644 --- a/google/cloud/spanner_v1/types/transaction.py +++ b/google/cloud/spanner_v1/types/transaction.py @@ -21,6 +21,8 @@ from google.protobuf import timestamp_pb2 # type: ignore import proto # type: ignore +from google.cloud.spanner_v1.types import location + __protobuf__ = proto.module( package="google.spanner.v1", manifest={ @@ -156,38 +158,42 @@ class ReadLockMode(proto.Enum): READ_LOCK_MODE_UNSPECIFIED (0): Default value. + - If isolation level is + [SERIALIZABLE][google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE], + locking semantics default to ``PESSIMISTIC``. - If isolation level is [REPEATABLE_READ][google.spanner.v1.TransactionOptions.IsolationLevel.REPEATABLE_READ], - then it is an error to specify ``read_lock_mode``. Locking - semantics default to ``OPTIMISTIC``. No validation checks - are done for reads, except to validate that the data that - was served at the snapshot time is unchanged at commit - time in the following cases: - - 1. reads done as part of queries that use - ``SELECT FOR UPDATE`` - 2. reads done as part of statements with a - ``LOCK_SCANNED_RANGES`` hint - 3. reads done as part of DML statements - - - At all other isolation levels, if ``read_lock_mode`` is - the default value, then pessimistic read locks are used. + locking semantics default to ``OPTIMISTIC``. + - See `Concurrency + control `__ + for more details. PESSIMISTIC (1): Pessimistic lock mode. - Read locks are acquired immediately on read. Semantics - described only applies to + Lock acquisition behavior depends on the isolation level in + use. In [SERIALIZABLE][google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE] - isolation. + isolation, reads and writes acquire necessary locks during + transaction statement execution. In + [REPEATABLE_READ][google.spanner.v1.TransactionOptions.IsolationLevel.REPEATABLE_READ] + isolation, reads that explicitly request to be locked and + writes acquire locks. See `Concurrency + control `__ + for details on the types of locks acquired at each + transaction step. OPTIMISTIC (2): Optimistic lock mode. - Locks for reads within the transaction are not acquired on - read. Instead the locks are acquired on a commit to validate - that read/queried data has not changed since the transaction - started. Semantics described only applies to + Lock acquisition behavior depends on the isolation level in + use. In both [SERIALIZABLE][google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE] - isolation. + and + [REPEATABLE_READ][google.spanner.v1.TransactionOptions.IsolationLevel.REPEATABLE_READ] + isolation, reads and writes do not acquire locks during + transaction statement execution. See `Concurrency + control `__ + for details on how the guarantees of each isolation level + are provided at commit time. """ READ_LOCK_MODE_UNSPECIFIED = 0 PESSIMISTIC = 1 @@ -383,6 +389,14 @@ class Transaction(proto.Message): this transaction attempt should be passed to the [Commit][google.spanner.v1.Spanner.Commit] request for this transaction. + cache_update (google.cloud.spanner_v1.types.CacheUpdate): + Optional. A cache update expresses a set of changes the + client should incorporate into its location cache. The + client should discard the changes if they are older than the + data it already has. This data can be obtained in response + to requests that included a ``RoutingHint`` field, but may + also be obtained by explicit location-fetching RPCs which + may be added in the future. """ id: bytes = proto.Field( @@ -399,6 +413,11 @@ class Transaction(proto.Message): number=3, message="MultiplexedSessionPrecommitToken", ) + cache_update: location.CacheUpdate = proto.Field( + proto.MESSAGE, + number=5, + message=location.CacheUpdate, + ) class TransactionSelector(proto.Message): diff --git a/tests/unit/test_client_context.py b/tests/unit/test_client_context.py index 3d03d30cc6..6e08b02d03 100644 --- a/tests/unit/test_client_context.py +++ b/tests/unit/test_client_context.py @@ -22,7 +22,6 @@ _merge_request_options, ) from google.cloud.spanner_v1.types import ( - ClientContext, ExecuteSqlRequest, RequestOptions, ) @@ -33,23 +32,27 @@ def test__merge_client_context_both_none(self): self.assertIsNone(_merge_client_context(None, None)) def test__merge_client_context_base_none(self): - merge = ClientContext(secure_context={"a": struct_pb2.Value(string_value="A")}) + merge = RequestOptions.ClientContext( + secure_context={"a": struct_pb2.Value(string_value="A")} + ) result = _merge_client_context(None, merge) self.assertEqual(result.secure_context["a"], "A") def test__merge_client_context_merge_none(self): - base = ClientContext(secure_context={"a": struct_pb2.Value(string_value="A")}) + base = RequestOptions.ClientContext( + secure_context={"a": struct_pb2.Value(string_value="A")} + ) result = _merge_client_context(base, None) self.assertEqual(result.secure_context["a"], "A") def test__merge_client_context_both_set(self): - base = ClientContext( + base = RequestOptions.ClientContext( secure_context={ "a": struct_pb2.Value(string_value="A"), "b": struct_pb2.Value(string_value="B1"), } ) - merge = ClientContext( + merge = RequestOptions.ClientContext( secure_context={ "b": struct_pb2.Value(string_value="B2"), "c": struct_pb2.Value(string_value="C"), @@ -62,7 +65,7 @@ def test__merge_client_context_both_set(self): def test__merge_request_options_with_client_context(self): request_options = RequestOptions(priority=RequestOptions.Priority.PRIORITY_LOW) - client_context = ClientContext( + client_context = RequestOptions.ClientContext( secure_context={"a": struct_pb2.Value(string_value="A")} ) @@ -91,7 +94,7 @@ def test_client_init_with_client_context(self): disable_builtin_metrics=True, ) - self.assertIsInstance(client._client_context, ClientContext) + self.assertIsInstance(client._client_context, RequestOptions.ClientContext) self.assertEqual(client._client_context.secure_context["a"], "A") def test_snapshot_execute_sql_propagates_client_context(self): @@ -106,11 +109,11 @@ def test_snapshot_execute_sql_propagates_client_context(self): client = database._instance._client = mock.Mock() client._query_options = None - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - snapshot_context = ClientContext( + snapshot_context = RequestOptions.ClientContext( secure_context={"snapshot": struct_pb2.Value(string_value="from-snapshot")} ) snapshot = Snapshot(session, client_context=snapshot_context) @@ -148,11 +151,11 @@ def test_transaction_commit_propagates_client_context(self): database._next_nth_request = 1 client = database._instance._client = mock.Mock() - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - transaction_context = ClientContext( + transaction_context = RequestOptions.ClientContext( secure_context={"txn": struct_pb2.Value(string_value="from-txn")} ) transaction = Transaction(session, client_context=transaction_context) @@ -196,11 +199,11 @@ def test_snapshot_execute_sql_request_level_override(self): client = database._instance._client = mock.Mock() client._query_options = None - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"a": struct_pb2.Value(string_value="from-client")} ) - snapshot_context = ClientContext( + snapshot_context = RequestOptions.ClientContext( secure_context={ "a": struct_pb2.Value(string_value="from-snapshot"), "b": struct_pb2.Value(string_value="B"), @@ -209,7 +212,7 @@ def test_snapshot_execute_sql_request_level_override(self): snapshot = Snapshot(session, client_context=snapshot_context) request_options = RequestOptions( - client_context=ClientContext( + client_context=RequestOptions.ClientContext( secure_context={"a": struct_pb2.Value(string_value="from-request")} ) ) @@ -241,11 +244,11 @@ def test_batch_commit_propagates_client_context(self): database.with_error_augmentation.return_value = (None, mock.MagicMock()) database._next_nth_request = 1 client = database._instance._client = mock.Mock() - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - batch_context = ClientContext( + batch_context = RequestOptions.ClientContext( secure_context={"batch": struct_pb2.Value(string_value="from-batch")} ) batch = Batch(session, client_context=batch_context) @@ -285,11 +288,11 @@ def test_transaction_execute_update_propagates_client_context(self): client = database._instance._client = mock.Mock() client._query_options = None - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - transaction_context = ClientContext( + transaction_context = RequestOptions.ClientContext( secure_context={"txn": struct_pb2.Value(string_value="from-txn")} ) transaction = Transaction(session, client_context=transaction_context) @@ -329,11 +332,11 @@ def test_mutation_groups_batch_write_propagates_client_context(self): database._next_nth_request = 1 client = database._instance._client = mock.Mock() - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - mg_context = ClientContext( + mg_context = RequestOptions.ClientContext( secure_context={"mg": struct_pb2.Value(string_value="from-mg")} ) mg = MutationGroups(session, client_context=mg_context) @@ -363,11 +366,11 @@ def test_batch_snapshot_propagates_client_context(self): database.name = "database-name" client = database._instance._client = mock.Mock() client._query_options = None - client._client_context = ClientContext( + client._client_context = RequestOptions.ClientContext( secure_context={"client": struct_pb2.Value(string_value="from-client")} ) - batch_context = ClientContext( + batch_context = RequestOptions.ClientContext( secure_context={"batch": struct_pb2.Value(string_value="from-batch")} ) batch_snapshot = BatchSnapshot(database, client_context=batch_context)