"""Spanner configuration."""
import contextlib
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypedDict, cast
from typing_extensions import NotRequired
from sqlspec.adapters.spanner._typing import SpannerAbstractSessionPool as AbstractSessionPool
from sqlspec.adapters.spanner._typing import SpannerAsyncAbstractSessionPool as AsyncAbstractSessionPool
from sqlspec.adapters.spanner._typing import SpannerAsyncBurstyPool as AsyncBurstyPool
from sqlspec.adapters.spanner._typing import SpannerAsyncClient as AsyncClient
from sqlspec.adapters.spanner._typing import (
SpannerAsyncConnection,
SpannerAsyncSessionContext,
SpannerAsyncTransactionType,
SpannerGoogleAPICallError,
SpannerSyncConnection,
SpannerSyncSessionContext,
SpannerTransactionType,
)
from sqlspec.adapters.spanner._typing import SpannerAsyncFixedSizePool as AsyncFixedSizePool
from sqlspec.adapters.spanner._typing import SpannerAsyncPingingPool as AsyncPingingPool
from sqlspec.adapters.spanner._typing import SpannerBurstyPool as BurstyPool
from sqlspec.adapters.spanner._typing import SpannerClient as Client
from sqlspec.adapters.spanner._typing import SpannerFixedSizePool as FixedSizePool
from sqlspec.adapters.spanner._typing import SpannerPingingPool as PingingPool
from sqlspec.adapters.spanner.core import (
apply_driver_features,
build_session_driver_features,
default_statement_config,
resolve_transaction_completion,
run_in_transaction_async,
run_in_transaction_sync,
)
from sqlspec.adapters.spanner.driver import SpannerAsyncDriver, SpannerSyncDriver
from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
from sqlspec.core import TypeCoercionCapabilities
from sqlspec.driver import (
AsyncPoolConnectionContext,
AsyncPoolSessionFactory,
SyncPoolConnectionContext,
SyncPoolSessionFactory,
)
from sqlspec.exceptions import ImproperConfigurationError
from sqlspec.extensions.events import EventRuntimeHints
from sqlspec.utils.config_tools import normalize_connection_config
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Mapping
from logging import Logger
from types import TracebackType
from sqlspec.adapters.spanner._typing import SpannerAsyncDatabase as AsyncDatabase
from sqlspec.adapters.spanner._typing import SpannerClientInfo as ClientInfo
from sqlspec.adapters.spanner._typing import SpannerClientOptions as ClientOptions
from sqlspec.adapters.spanner._typing import SpannerCredentials as Credentials
from sqlspec.adapters.spanner._typing import SpannerDatabase as Database
from sqlspec.adapters.spanner._typing import SpannerDatabaseDialect as DatabaseDialect
from sqlspec.adapters.spanner._typing import SpannerDefaultTransactionOptions as DefaultTransactionOptions
from sqlspec.adapters.spanner._typing import SpannerDirectedReadOptions as DirectedReadOptions
from sqlspec.adapters.spanner._typing import SpannerEncryptionConfig as EncryptionConfig
from sqlspec.adapters.spanner._typing import SpannerExecuteSqlRequest as ExecuteSqlRequest
from sqlspec.adapters.spanner._typing import SpannerRequestOptions as RequestOptions
from sqlspec.adapters.spanner._typing import SpannerRetry as Retry
from sqlspec.config import ExtensionConfigs
from sqlspec.core import StatementConfig
from sqlspec.observability import ObservabilityConfig
__all__ = (
"SpannerAsyncConfig",
"SpannerConnectionParams",
"SpannerDriverFeatures",
"SpannerPoolParams",
"SpannerSyncConfig",
"build_connection_config",
)
_DEFAULT_SESSION_TRANSACTION: bool = True
"""Default ``transaction`` flag for ``provide_session`` / ``provide_connection``.
``True`` yields a write-capable ``Transaction`` context; read-only
``Snapshot`` contexts are available through ``provide_read_session``."""
_CLIENT_CONFIG_FIELDS = frozenset({
"project",
"credentials",
"client_info",
"client_options",
"query_options",
"route_to_leader_enabled",
"directed_read_options",
"observability_options",
"default_transaction_options",
"experimental_host",
"disable_builtin_metrics",
"client_context",
"use_plain_text",
"ca_certificate",
"client_certificate",
"client_key",
"instance_type",
})
_INSTANCE_CONFIG_FIELDS = frozenset({"configuration_name", "display_name", "node_count", "processing_units"})
_DATABASE_CONFIG_FIELDS = frozenset({
"ddl_statements",
"logger",
"encryption_config",
"database_dialect",
"database_role",
"enable_drop_protection",
"enable_interceptors_in_tests",
"proto_descriptors",
})
[docs]
class SpannerConnectionParams(TypedDict):
"""Spanner connection parameters."""
project: "NotRequired[str]"
project_id: "NotRequired[str]"
credentials: "NotRequired[Credentials]"
client_info: "NotRequired[ClientInfo]"
client_options: "NotRequired[ClientOptions | dict[str, Any]]"
query_options: "NotRequired[ExecuteSqlRequest.QueryOptions]"
route_to_leader_enabled: "NotRequired[bool]"
directed_read_options: "NotRequired[DirectedReadOptions]"
observability_options: "NotRequired[Any]"
default_transaction_options: "NotRequired[DefaultTransactionOptions]"
experimental_host: "NotRequired[str]"
disable_builtin_metrics: "NotRequired[bool]"
client_context: "NotRequired[dict[str, str]]"
use_plain_text: "NotRequired[bool]"
ca_certificate: "NotRequired[str]"
client_certificate: "NotRequired[str]"
client_key: "NotRequired[str]"
instance_type: "NotRequired[str]"
instance_id: "NotRequired[str]"
instance: "NotRequired[str]"
configuration_name: "NotRequired[str]"
display_name: "NotRequired[str]"
node_count: "NotRequired[int]"
processing_units: "NotRequired[int]"
instance_labels: "NotRequired[dict[str, str]]"
database_id: "NotRequired[str]"
database: "NotRequired[str]"
db: "NotRequired[str]"
ddl_statements: "NotRequired[tuple[str, ...] | list[str]]"
logger: "NotRequired[Logger]"
encryption_config: "NotRequired[EncryptionConfig | dict[str, Any]]"
database_dialect: "NotRequired[DatabaseDialect]"
database_role: "NotRequired[str]"
enable_drop_protection: "NotRequired[bool]"
enable_interceptors_in_tests: "NotRequired[bool]"
proto_descriptors: "NotRequired[bytes]"
extra: "NotRequired[dict[str, Any]]"
[docs]
class SpannerPoolParams(SpannerConnectionParams):
"""Session pool configuration.
``pool_type`` must be a sync pool class for :class:`SpannerSyncConfig`
(default ``FixedSizePool``) and an async pool class for
:class:`SpannerAsyncConfig` (default async ``FixedSizePool``).
"""
pool_type: "NotRequired[type[AbstractSessionPool | AsyncAbstractSessionPool]]"
size: "NotRequired[int]"
target_size: "NotRequired[int]"
max_sessions: "NotRequired[int]"
default_timeout: "NotRequired[int | float]"
session_labels: "NotRequired[dict[str, str]]"
labels: "NotRequired[dict[str, str]]"
ping_interval: "NotRequired[int]"
max_age_minutes: "NotRequired[int]"
[docs]
class SpannerDriverFeatures(TypedDict):
"""Driver feature flags for Spanner.
Attributes:
enable_uuid_conversion: Enable automatic UUID string conversion.
json_serializer: Custom JSON serializer for parameter conversion.
json_deserializer: Custom JSON deserializer for result conversion.
retry: Per-request retry policy passed to execute_sql(), execute_update(), and batch_update().
timeout: Per-request timeout in seconds passed to execute_sql(), execute_update(), and batch_update().
request_options: Default RequestOptions forwarded to execute_sql(), execute_update(),
and batch_update(). Per-call overrides are available through normal
driver execution methods.
directed_read_options: Default DirectedReadOptions forwarded to execute_sql().
session_labels: Deprecated compatibility alias for pool session labels.
Prefer ``connection_config["session_labels"]``.
enable_events: Enable database event channel support.
Defaults to True when extension_config["events"] is configured.
events_backend: Backend type for event handling.
Spanner only supports "poll_queue" (no native pub/sub).
enable_batch_write_api: Route load_from_arrow through the Spanner Batch Write API
(Database.mutation_groups().batch_write()) for high-throughput, independently
committed mutation groups instead of a single in-transaction insert_or_update.
Defaults to False.
"""
enable_uuid_conversion: "NotRequired[bool]"
json_serializer: "NotRequired[Callable[[Any], str]]"
json_deserializer: "NotRequired[Callable[[str], Any]]"
retry: "NotRequired[Retry | None]"
timeout: "NotRequired[float | None]"
query_options: "NotRequired[ExecuteSqlRequest.QueryOptions | dict[str, Any] | None]"
request_options: "NotRequired[RequestOptions | dict[str, Any] | None]"
directed_read_options: "NotRequired[DirectedReadOptions | None]"
session_labels: "NotRequired[dict[str, str]]"
enable_events: "NotRequired[bool]"
events_backend: "NotRequired[Literal['poll_queue']]"
enable_batch_write_api: "NotRequired[bool]"
def build_connection_config(
connection_config: "SpannerPoolParams | dict[str, Any] | Mapping[str, Any] | None",
) -> dict[str, Any]:
"""Normalize Spanner connection configuration and map aliases."""
config = normalize_connection_config(connection_config)
project_alias = config.pop("project_id", None)
if project_alias is not None and "project" not in config:
config["project"] = project_alias
instance_alias = config.pop("instance", None)
if instance_alias is not None and "instance_id" not in config:
config["instance_id"] = instance_alias
database_alias = config.pop("database", None) or config.pop("db", None)
if database_alias is not None and "database_id" not in config:
config["database_id"] = database_alias
return config
def _prepare_settings(
connection_config: "SpannerPoolParams | dict[str, Any] | None",
driver_features: "SpannerDriverFeatures | dict[str, Any] | None",
*,
default_pool_type: type[Any],
) -> "tuple[dict[str, Any], dict[str, Any]]":
"""Normalize connection settings and move legacy session labels onto the pool settings."""
config = build_connection_config(connection_config)
if "min_sessions" in config:
msg = "Spanner session pools do not support 'min_sessions'; use 'size' or 'target_size'."
raise ImproperConfigurationError(msg)
raw_driver_features: dict[str, Any] = dict(driver_features) if driver_features else {}
legacy_session_labels = raw_driver_features.pop("session_labels", None)
if legacy_session_labels is not None and "session_labels" not in config and "labels" not in config:
config["session_labels"] = legacy_session_labels
config.setdefault("size", config.pop("max_sessions", 10))
config.setdefault("pool_type", default_pool_type)
return config, raw_driver_features
def _require_database_ids(connection_config: "dict[str, Any]") -> "tuple[str, str]":
instance_id = connection_config.get("instance_id")
database_id = connection_config.get("database_id")
if not instance_id or not database_id:
msg = "instance_id and database_id are required."
raise ImproperConfigurationError(msg)
return instance_id, database_id
def _connection_kwargs_for(connection_config: "dict[str, Any]", fields: "frozenset[str] | set[str]") -> dict[str, Any]:
return {field: connection_config[field] for field in fields if connection_config.get(field) is not None}
def _instance_kwargs(connection_config: "dict[str, Any]") -> dict[str, Any]:
instance_kwargs = _connection_kwargs_for(connection_config, _INSTANCE_CONFIG_FIELDS)
instance_labels = connection_config.get("instance_labels")
if instance_labels is not None:
instance_kwargs["labels"] = instance_labels
return instance_kwargs
def _resolve_pool_type(
connection_config: "dict[str, Any]", *, base: type[Any], config_name: str, expected: str
) -> type[Any]:
pool_type = connection_config["pool_type"]
if not isinstance(pool_type, type) or not issubclass(pool_type, base):
msg = f"{config_name} requires {expected} session pool class; got {pool_type!r}."
raise ImproperConfigurationError(msg)
return pool_type
def _pool_kwargs(connection_config: "dict[str, Any]", pool_kind: str) -> dict[str, Any]:
"""Build session pool constructor arguments for a pool kind.
Args:
connection_config: Normalized connection settings.
pool_kind: ``"pinging"``, ``"fixed"``, ``"bursty"``, or ``"custom"``.
Returns:
Keyword arguments for the pool class.
"""
pool_kwargs: dict[str, Any] = {}
labels = connection_config.get("session_labels", connection_config.get("labels"))
if labels is not None:
pool_kwargs["labels"] = labels
database_role = connection_config.get("database_role")
if database_role is not None:
pool_kwargs["database_role"] = database_role
if pool_kind == "pinging":
pool_kwargs.update(_connection_kwargs_for(connection_config, {"size", "default_timeout"}))
pool_kwargs["ping_interval"] = connection_config.get("ping_interval", 1800)
elif pool_kind == "fixed":
pool_kwargs.update(_connection_kwargs_for(connection_config, {"size", "default_timeout", "max_age_minutes"}))
elif pool_kind == "bursty":
target_size = connection_config.get("target_size", connection_config.get("size"))
if target_size is not None:
pool_kwargs["target_size"] = target_size
else:
pool_kwargs.update(
_connection_kwargs_for(
connection_config, {"size", "target_size", "default_timeout", "ping_interval", "max_age_minutes"}
)
)
return pool_kwargs
class SpannerSyncConnectionContext(SyncPoolConnectionContext):
"""Context manager for sync Spanner connections (Snapshot or Transaction)."""
__slots__ = ("_connection", "_session", "_transaction")
def __init__(self, config: "SpannerSyncConfig", transaction: bool = False) -> None:
super().__init__(config)
self._transaction = transaction
self._connection: SpannerSyncConnection | None = None
self._session: Any = None
def __enter__(self) -> "SpannerSyncConnection":
database = self._config.get_database()
if self._transaction:
manager = cast("Any", database).sessions_manager
self._session = manager.get_session(SpannerTransactionType.READ_WRITE)
try:
txn = self._session.transaction()
txn.__enter__()
except Exception:
manager.put_session(self._session)
self._session = None
raise
self._connection = cast("SpannerSyncConnection", txn)
return self._connection
self._session = cast("Any", database).snapshot(multi_use=True)
self._connection = cast("SpannerSyncConnection", self._session.__enter__())
return self._connection
def __exit__(
self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
) -> "bool | None":
if self._transaction:
try:
if self._connection is not None:
txn = cast("Any", self._connection)
action = resolve_transaction_completion(txn, failed=exc_type is not None)
if action == "commit":
txn.commit()
elif action == "rollback":
txn.rollback()
finally:
if self._session is not None:
cast("Any", self._config.get_database()).sessions_manager.put_session(self._session)
self._connection = None
self._session = None
return None
if self._session is not None:
self._session.__exit__(exc_type, exc_val, exc_tb)
self._connection = None
self._session = None
return None
class _SpannerSyncSessionConnectionHandler(SyncPoolSessionFactory):
"""Sync session handler that uses SpannerSyncConnectionContext."""
__slots__ = ("_transaction",)
def __init__(self, config: "SpannerSyncConfig", transaction: bool = False) -> None:
super().__init__(config)
self._transaction = transaction
def acquire_connection(self) -> "SpannerSyncConnection":
self._ctx = SpannerSyncConnectionContext(self._config, transaction=self._transaction)
return cast("SpannerSyncConnectionContext", self._ctx).__enter__()
def release_connection(self, _conn: "SpannerSyncConnection", **kwargs: Any) -> None:
if self._ctx is not None:
context = cast("SpannerSyncConnectionContext", self._ctx)
context._connection = _conn
context.__exit__(kwargs.get("exc_type"), kwargs.get("exc_val"), kwargs.get("exc_tb"))
self._ctx = None
[docs]
class SpannerSyncConfig(SyncDatabaseConfig["SpannerSyncConnection", "AbstractSessionPool", SpannerSyncDriver]):
"""Spanner configuration and session management."""
driver_type: ClassVar[type["SpannerSyncDriver"]] = SpannerSyncDriver
connection_type: ClassVar[type["SpannerSyncConnection"]] = cast(
"type[SpannerSyncConnection]", SpannerSyncConnection
)
supports_transactional_ddl: ClassVar[bool] = False
supports_native_arrow_export: ClassVar[bool] = True
supports_native_arrow_import: ClassVar[bool] = True
supports_native_parquet_export: ClassVar[bool] = False
supports_native_parquet_import: ClassVar[bool] = False
type_coercion_capabilities: ClassVar[TypeCoercionCapabilities] = TypeCoercionCapabilities(
datetime_binding="native", timestamp_precision="microsecond", json_columns_decoded=True, uuid_binding="text"
)
_connection_context_class: "ClassVar[type[SpannerSyncConnectionContext]]" = SpannerSyncConnectionContext
_session_factory_class: "ClassVar[type[_SpannerSyncSessionConnectionHandler]]" = (
_SpannerSyncSessionConnectionHandler
)
_session_context_class: "ClassVar[type[SpannerSyncSessionContext]]" = SpannerSyncSessionContext
_default_statement_config = default_statement_config
_DEFAULT_SESSION_TRANSACTION: ClassVar[bool] = _DEFAULT_SESSION_TRANSACTION
[docs]
def __init__(
self,
*,
connection_config: "SpannerPoolParams | dict[str, Any] | None" = None,
connection_instance: "AbstractSessionPool | None" = None,
migration_config: "dict[str, Any] | None" = None,
statement_config: "StatementConfig | None" = None,
driver_features: "SpannerDriverFeatures | dict[str, Any] | None" = None,
bind_key: "str | None" = None,
extension_config: "ExtensionConfigs | None" = None,
observability_config: "ObservabilityConfig | None" = None,
**kwargs: Any,
) -> None:
self.connection_config, raw_driver_features = _prepare_settings(
connection_config, driver_features, default_pool_type=FixedSizePool
)
statement_config, processed_features = apply_driver_features(
statement_config or default_statement_config, raw_driver_features
)
super().__init__(
connection_config=self.connection_config,
connection_instance=connection_instance,
migration_config=migration_config,
statement_config=statement_config,
driver_features=processed_features,
bind_key=bind_key,
extension_config=extension_config,
observability_config=observability_config,
**kwargs,
)
self._client: Client | None = None
self._database: Database | None = None
def _get_client(self) -> "Client":
if self._client is None:
self._client = Client(**_connection_kwargs_for(self.connection_config, _CLIENT_CONFIG_FIELDS))
return self._client
[docs]
def get_database(self) -> "Database":
"""Return the configured database, creating the pool and client on first use.
Returns:
The Spanner database bound to the configured session pool.
Raises:
ImproperConfigurationError: If ``instance_id`` or ``database_id`` is missing.
"""
instance_id, database_id = _require_database_ids(self.connection_config)
if self.connection_instance is None:
self.connection_instance = self.provide_pool()
if self._database is None:
instance = cast("Any", self._get_client()).instance(instance_id, **_instance_kwargs(self.connection_config))
self._database = cast(
"Database",
instance.database(
database_id,
pool=self.connection_instance,
**_connection_kwargs_for(self.connection_config, _DATABASE_CONFIG_FIELDS),
),
)
return self._database
[docs]
def create_connection(self) -> "SpannerSyncConnection":
"""Return a read-only snapshot checkout owned by the caller.
The checkout borrows a pooled session when entered and returns it on exit.
Returns:
A snapshot checkout to be used as a context manager.
"""
return cast("SpannerSyncConnection", cast("Any", self.get_database()).snapshot(multi_use=True))
def _create_pool(self) -> "AbstractSessionPool":
_require_database_ids(self.connection_config)
pool_type = _resolve_pool_type(
self.connection_config, base=AbstractSessionPool, config_name="SpannerSyncConfig", expected="a sync"
)
if issubclass(pool_type, PingingPool):
pool_kind = "pinging"
elif issubclass(pool_type, FixedSizePool):
pool_kind = "fixed"
elif issubclass(pool_type, BurstyPool):
pool_kind = "bursty"
else:
pool_kind = "custom"
return cast("AbstractSessionPool", pool_type(**_pool_kwargs(self.connection_config, pool_kind)))
def _close_pool(self) -> None:
"""Release pooled sessions, then close the database and client."""
pool = self.connection_instance
if pool is not None:
with contextlib.suppress(SpannerGoogleAPICallError):
pool.clear() # type: ignore[no-untyped-call]
if self._database is not None:
with contextlib.suppress(SpannerGoogleAPICallError):
self._database.close() # type: ignore[no-untyped-call]
if self._client is not None:
self._client.close() # type: ignore[no-untyped-call]
self._client = None
self._database = None
[docs]
def provide_connection(
self, *args: Any, transaction: "bool" = _DEFAULT_SESSION_TRANSACTION, **kwargs: Any
) -> "SpannerSyncConnectionContext":
"""Yield a Transaction (default) or Snapshot context from the configured pool.
Args:
*args: Additional positional arguments (unused, for interface compatibility).
transaction: If True (default), yields a Transaction context that
supports execute_update() for DML statements. If False, yields
a read-only Snapshot context for SELECT queries.
**kwargs: Additional keyword arguments (unused, for interface compatibility).
Returns:
A Spanner connection context manager.
"""
return SpannerSyncConnectionContext(self, transaction=transaction)
[docs]
def provide_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
transaction: "bool" = _DEFAULT_SESSION_TRANSACTION,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerSyncSessionContext":
"""Provide a Spanner driver session context manager.
Returns a write-capable Transaction session by default. Pass
``transaction=False`` or use :meth:`provide_read_session` to obtain a
read-only Snapshot session.
Args:
*args: Additional arguments.
statement_config: Optional statement configuration override.
transaction: Whether to use a Transaction (True, default) or
Snapshot (False).
request_options: Session-scoped RequestOptions for Spanner statements.
directed_read_options: Session-scoped DirectedReadOptions for reads.
query_options: Session-scoped QueryOptions for Spanner statements.
retry: Session-scoped retry policy for Spanner statement calls.
timeout: Session-scoped timeout for Spanner statement calls.
**kwargs: Additional keyword arguments.
Returns:
A Spanner driver session context manager.
"""
handler = _SpannerSyncSessionConnectionHandler(self, transaction=transaction)
return SpannerSyncSessionContext(
acquire_connection=handler.acquire_connection,
release_connection=handler.release_connection,
statement_config=statement_config or self.statement_config or default_statement_config,
driver_features=build_session_driver_features(
self.driver_features,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
),
prepare_driver=self._prepare_driver,
)
[docs]
def provide_write_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerSyncSessionContext":
"""Provide a write-capable Spanner session (alias for :meth:`provide_session`)."""
return self.provide_session(
*args,
statement_config=statement_config,
transaction=True,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
**kwargs,
)
[docs]
def provide_read_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerSyncSessionContext":
"""Provide a read-only Snapshot Spanner session.
Use for query workloads that benefit from Spanner's snapshot reads.
For DDL/DML, use :meth:`provide_session` (write-capable by default).
"""
return self.provide_session(
*args,
statement_config=statement_config,
transaction=False,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
**kwargs,
)
[docs]
def run_in_transaction(self, fn: "Callable[..., Any]", *args: Any, **kwargs: Any) -> Any:
"""Execute a callable inside Spanner's retryable transaction runner.
``fn`` is called as ``fn(driver, *args, **kwargs)`` with a driver bound
to the runner's transaction. A ``DeadlockError`` caused by
``google.api_core.exceptions.Aborted`` is unwrapped so the runner
retries, and a terminal ``Aborted`` is raised as ``DeadlockError``.
"""
return run_in_transaction_sync(self.get_database(), self._transaction_driver, fn, *args, **kwargs)
def _transaction_driver(self, transaction: "SpannerSyncConnection") -> "SpannerSyncDriver":
driver = self.driver_type(
connection=transaction, statement_config=self.statement_config, driver_features=self.driver_features
)
driver._owns_transaction = False # pyright: ignore[reportPrivateUsage]
return self._prepare_driver(driver)
[docs]
def get_signature_namespace(self) -> "dict[str, Any]":
"""Get the signature namespace for SpannerSyncConfig types.
Returns:
Dictionary mapping type names to types.
"""
namespace = super().get_signature_namespace()
namespace.update({
"SpannerConnectionParams": SpannerConnectionParams,
"SpannerDriverFeatures": SpannerDriverFeatures,
"SpannerPoolParams": SpannerPoolParams,
"SpannerSyncConfig": SpannerSyncConfig,
"SpannerSyncConnection": SpannerSyncConnection,
"SpannerSyncConnectionContext": SpannerSyncConnectionContext,
"SpannerSyncDriver": SpannerSyncDriver,
"SpannerSyncSessionContext": SpannerSyncSessionContext,
})
return namespace
[docs]
def get_event_runtime_hints(self) -> "EventRuntimeHints":
"""Return queue defaults for Spanner JSON and transaction handling."""
return EventRuntimeHints(cleanup_on_ack=False, use_run_in_transaction=True)
class SpannerAsyncConnectionContext(AsyncPoolConnectionContext):
"""Context manager for async Spanner connections (Snapshot or Transaction)."""
__slots__ = ("_session", "_transaction")
def __init__(self, config: "SpannerAsyncConfig", transaction: bool = False) -> None:
super().__init__(config)
self._transaction = transaction
self._connection: SpannerAsyncConnection | None = None
self._session: Any = None
async def __aenter__(self) -> "SpannerAsyncConnection":
database = await self._config.get_database()
if self._transaction:
manager = cast("Any", database).sessions_manager
self._session = await manager.get_session(SpannerAsyncTransactionType.READ_WRITE)
try:
txn = self._session.transaction()
await txn.__aenter__()
except Exception:
await manager.put_session(self._session)
self._session = None
raise
self._connection = cast("SpannerAsyncConnection", txn)
return self._connection
self._session = cast("Any", database).snapshot(multi_use=True)
self._connection = cast("SpannerAsyncConnection", await self._session.__aenter__())
return self._connection
async def __aexit__(
self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
) -> "bool | None":
if self._transaction:
try:
if self._connection is not None:
txn = cast("Any", self._connection)
action = resolve_transaction_completion(txn, failed=exc_type is not None)
if action == "commit":
await txn.commit()
elif action == "rollback":
await txn.rollback()
finally:
if self._session is not None:
database = await self._config.get_database()
await cast("Any", database).sessions_manager.put_session(self._session)
self._connection = None
self._session = None
return None
if self._session is not None:
await self._session.__aexit__(exc_type, exc_val, exc_tb)
self._connection = None
self._session = None
return None
class _SpannerAsyncSessionConnectionHandler(AsyncPoolSessionFactory):
"""Async session handler that uses SpannerAsyncConnectionContext."""
__slots__ = ("_ctx", "_transaction")
def __init__(self, config: "SpannerAsyncConfig", transaction: bool = False) -> None:
super().__init__(config)
self._transaction = transaction
self._ctx: SpannerAsyncConnectionContext | None = None
async def acquire_connection(self) -> "SpannerAsyncConnection":
self._ctx = SpannerAsyncConnectionContext(self._config, transaction=self._transaction)
return await self._ctx.__aenter__()
async def release_connection(self, _conn: "SpannerAsyncConnection", **kwargs: Any) -> None:
if self._ctx is not None:
self._ctx._connection = _conn
await self._ctx.__aexit__(kwargs.get("exc_type"), kwargs.get("exc_val"), kwargs.get("exc_tb"))
self._ctx = None
[docs]
class SpannerAsyncConfig(AsyncDatabaseConfig["SpannerAsyncConnection", "AsyncAbstractSessionPool", SpannerAsyncDriver]):
"""Async Spanner configuration and session management."""
driver_type: ClassVar[type["SpannerAsyncDriver"]] = SpannerAsyncDriver
connection_type: ClassVar[type["SpannerAsyncConnection"]] = cast(
"type[SpannerAsyncConnection]", SpannerAsyncConnection
)
supports_transactional_ddl: ClassVar[bool] = False
supports_native_arrow_export: ClassVar[bool] = True
supports_native_arrow_import: ClassVar[bool] = True
supports_native_parquet_export: ClassVar[bool] = False
supports_native_parquet_import: ClassVar[bool] = False
type_coercion_capabilities: ClassVar[TypeCoercionCapabilities] = TypeCoercionCapabilities(
datetime_binding="native", timestamp_precision="microsecond", json_columns_decoded=True, uuid_binding="text"
)
_connection_context_class: "ClassVar[type[SpannerAsyncConnectionContext]]" = SpannerAsyncConnectionContext
_session_factory_class: "ClassVar[type[_SpannerAsyncSessionConnectionHandler]]" = (
_SpannerAsyncSessionConnectionHandler
)
_session_context_class: "ClassVar[type[SpannerAsyncSessionContext]]" = SpannerAsyncSessionContext
_default_statement_config = default_statement_config
_DEFAULT_SESSION_TRANSACTION: ClassVar[bool] = _DEFAULT_SESSION_TRANSACTION
[docs]
def __init__(
self,
*,
connection_config: "SpannerPoolParams | dict[str, Any] | None" = None,
connection_instance: "AsyncAbstractSessionPool | None" = None,
migration_config: "dict[str, Any] | None" = None,
statement_config: "StatementConfig | None" = None,
driver_features: "SpannerDriverFeatures | dict[str, Any] | None" = None,
bind_key: "str | None" = None,
extension_config: "ExtensionConfigs | None" = None,
observability_config: "ObservabilityConfig | None" = None,
**kwargs: Any,
) -> None:
self.connection_config, raw_driver_features = _prepare_settings(
connection_config, driver_features, default_pool_type=AsyncFixedSizePool
)
statement_config, processed_features = apply_driver_features(
statement_config or default_statement_config, raw_driver_features
)
super().__init__(
connection_config=self.connection_config,
connection_instance=connection_instance,
migration_config=migration_config,
statement_config=statement_config,
driver_features=processed_features,
bind_key=bind_key,
extension_config=extension_config,
observability_config=observability_config,
**kwargs,
)
self._client: AsyncClient | None = None
self._database: AsyncDatabase | None = None
def _get_client(self) -> "AsyncClient":
if self._client is None:
self._client = AsyncClient(**_connection_kwargs_for(self.connection_config, _CLIENT_CONFIG_FIELDS))
return self._client
[docs]
async def get_database(self) -> "AsyncDatabase":
"""Return the configured database, creating the pool and client on first use.
Returns:
The async Spanner database bound to the configured session pool.
Raises:
ImproperConfigurationError: If ``instance_id`` or ``database_id`` is missing.
"""
instance_id, database_id = _require_database_ids(self.connection_config)
if self.connection_instance is None:
self.connection_instance = await self.provide_pool()
if self._database is None:
instance = cast("Any", self._get_client()).instance(instance_id, **_instance_kwargs(self.connection_config))
self._database = cast(
"AsyncDatabase",
await instance.database(
database_id,
pool=self.connection_instance,
**_connection_kwargs_for(self.connection_config, _DATABASE_CONFIG_FIELDS),
),
)
return self._database
[docs]
async def create_connection(self) -> "SpannerAsyncConnection":
"""Return a read-only async snapshot checkout owned by the caller.
The checkout borrows a pooled session when entered and returns it on exit.
Returns:
A snapshot checkout to be used as an async context manager.
"""
database = await self.get_database()
return cast("SpannerAsyncConnection", cast("Any", database).snapshot(multi_use=True))
async def _create_pool(self) -> "AsyncAbstractSessionPool":
_require_database_ids(self.connection_config)
pool_type = _resolve_pool_type(
self.connection_config, base=AsyncAbstractSessionPool, config_name="SpannerAsyncConfig", expected="an async"
)
if issubclass(pool_type, AsyncPingingPool):
pool_kind = "pinging"
elif issubclass(pool_type, AsyncFixedSizePool):
pool_kind = "fixed"
elif issubclass(pool_type, AsyncBurstyPool):
pool_kind = "bursty"
else:
pool_kind = "custom"
return cast("AsyncAbstractSessionPool", pool_type(**_pool_kwargs(self.connection_config, pool_kind)))
async def _close_pool(self) -> None:
"""Release pooled sessions, then close the database and client."""
pool = self.connection_instance
if pool is not None:
with contextlib.suppress(SpannerGoogleAPICallError):
await pool.clear() # type: ignore[no-untyped-call]
if self._database is not None:
with contextlib.suppress(SpannerGoogleAPICallError):
await self._database.close()
if self._client is not None:
self._client.close() # type: ignore[no-untyped-call]
self._client = None
self._database = None
[docs]
def provide_connection(
self, *args: Any, transaction: "bool" = _DEFAULT_SESSION_TRANSACTION, **kwargs: Any
) -> "SpannerAsyncConnectionContext":
"""Yield a Transaction (default) or Snapshot context from the configured pool.
Args:
*args: Additional positional arguments (unused, for interface compatibility).
transaction: If True (default), yields a Transaction context that
supports execute_update() for DML statements. If False, yields
a read-only Snapshot context for SELECT queries.
**kwargs: Additional keyword arguments (unused, for interface compatibility).
Returns:
An async Spanner connection context manager.
"""
return SpannerAsyncConnectionContext(self, transaction=transaction)
[docs]
def provide_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
transaction: "bool" = _DEFAULT_SESSION_TRANSACTION,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerAsyncSessionContext":
"""Provide an async Spanner driver session context manager.
Returns a write-capable Transaction session by default. Pass
``transaction=False`` or use :meth:`provide_read_session` to obtain a
read-only Snapshot session.
Args:
*args: Additional arguments.
statement_config: Optional statement configuration override.
transaction: Whether to use a Transaction (True, default) or
Snapshot (False).
request_options: Session-scoped RequestOptions for Spanner statements.
directed_read_options: Session-scoped DirectedReadOptions for reads.
query_options: Session-scoped QueryOptions for Spanner statements.
retry: Session-scoped retry policy for Spanner statement calls.
timeout: Session-scoped timeout for Spanner statement calls.
**kwargs: Additional keyword arguments.
Returns:
An async Spanner driver session context manager.
"""
handler = _SpannerAsyncSessionConnectionHandler(self, transaction=transaction)
return SpannerAsyncSessionContext(
acquire_connection=handler.acquire_connection,
release_connection=handler.release_connection,
statement_config=statement_config or self.statement_config or default_statement_config,
driver_features=build_session_driver_features(
self.driver_features,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
),
prepare_driver=self._prepare_driver,
)
[docs]
def provide_write_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerAsyncSessionContext":
"""Provide a write-capable async Spanner session (alias for :meth:`provide_session`)."""
return self.provide_session(
*args,
statement_config=statement_config,
transaction=True,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
**kwargs,
)
[docs]
def provide_read_session(
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
request_options: "RequestOptions | dict[str, Any] | None" = None,
directed_read_options: "DirectedReadOptions | None" = None,
query_options: "ExecuteSqlRequest.QueryOptions | dict[str, Any] | None" = None,
retry: "Retry | None" = None,
timeout: "float | None" = None,
**kwargs: Any,
) -> "SpannerAsyncSessionContext":
"""Provide a read-only Snapshot async Spanner session.
Use for query workloads that benefit from Spanner's snapshot reads.
For DDL/DML, use :meth:`provide_session` (write-capable by default).
"""
return self.provide_session(
*args,
statement_config=statement_config,
transaction=False,
request_options=request_options,
directed_read_options=directed_read_options,
query_options=query_options,
retry=retry,
timeout=timeout,
**kwargs,
)
[docs]
async def run_in_transaction(self, fn: "Callable[..., Awaitable[Any]]", *args: Any, **kwargs: Any) -> Any:
"""Execute a coroutine function inside Spanner's async retryable transaction runner.
``fn`` is awaited as ``fn(driver, *args, **kwargs)`` with a driver bound
to the runner's transaction. A ``DeadlockError`` caused by
``google.api_core.exceptions.Aborted`` is unwrapped so the runner
retries, and a terminal ``Aborted`` is raised as ``DeadlockError``.
"""
database = await self.get_database()
return await run_in_transaction_async(database, self._transaction_driver, fn, *args, **kwargs)
def _transaction_driver(self, transaction: "SpannerAsyncConnection") -> "SpannerAsyncDriver":
driver = self.driver_type(
connection=transaction, statement_config=self.statement_config, driver_features=self.driver_features
)
driver._owns_transaction = False # pyright: ignore[reportPrivateUsage]
return self._prepare_driver(driver)
[docs]
def get_signature_namespace(self) -> "dict[str, Any]":
"""Get the signature namespace for SpannerAsyncConfig types.
Returns:
Dictionary mapping type names to types.
"""
namespace = super().get_signature_namespace()
namespace.update({
"SpannerAsyncConfig": SpannerAsyncConfig,
"SpannerAsyncConnection": SpannerAsyncConnection,
"SpannerAsyncConnectionContext": SpannerAsyncConnectionContext,
"SpannerAsyncDriver": SpannerAsyncDriver,
"SpannerAsyncSessionContext": SpannerAsyncSessionContext,
"SpannerConnectionParams": SpannerConnectionParams,
"SpannerDriverFeatures": SpannerDriverFeatures,
"SpannerPoolParams": SpannerPoolParams,
})
return namespace
[docs]
def get_event_runtime_hints(self) -> "EventRuntimeHints":
"""Return queue defaults for Spanner JSON and transaction handling."""
return EventRuntimeHints(cleanup_on_ack=False, use_run_in_transaction=True)