Source code for sqlspec.adapters.spanner.adk.artifact_store

"""Spanner ADK artifact store (sync and async)."""

from typing import TYPE_CHECKING, Any, ClassVar, cast

from sqlspec.adapters.spanner._typing import SpannerGoogleAPICallError
from sqlspec.adapters.spanner._typing import SpannerNotFound as NotFound
from sqlspec.adapters.spanner.adk.store import (
    _DDL_TIMEOUT_SECONDS,
    SPANNER_PARAM_TYPES,
    _adk_config,
    _extract_spanner_json_codecs,
    _is_spanner_table_missing,
    _json_param_type,
    _run_read_async,
    _spanner_row_deletion_policy,
    _SpannerAsyncUpdateJob,
    _SpannerReadProtocol,
    _SpannerSyncUpdateJob,
    _SpannerWriteTransactionProtocol,
)
from sqlspec.adapters.spanner.config import SpannerAsyncConfig, SpannerSyncConfig
from sqlspec.adapters.spanner.core import (
    _convert_json_row_value,
    create_mapped_exception,
    execute_ddl_async,
    execute_ddl_sync,
    list_existing_table_names_async,
    list_existing_table_names_sync,
)
from sqlspec.exceptions import SQLSpecError
from sqlspec.extensions.adk.artifact._types import StoredArtifact
from sqlspec.extensions.adk.artifact.store import BaseAsyncADKArtifactStore, BaseSyncADKArtifactStore

if TYPE_CHECKING:
    from collections.abc import Callable
    from datetime import datetime

    from sqlspec.adapters.spanner._typing import (
        SpannerAsyncDatabase,
        SpannerAsyncTransaction,
        SpannerDatabase,
        SpannerTransaction,
    )

__all__ = ("USER_SCOPED_SESSION_ID", "SpannerAsyncADKArtifactStore", "SpannerSyncADKArtifactStore")

USER_SCOPED_SESSION_ID = ""


class _SpannerADKArtifactStoreMixin:
    """Shared SQL and conversion helpers for Spanner ADK artifact stores."""

    __slots__ = ()

    if TYPE_CHECKING:
        _artifact_table: str
        _artifact_row_deletion_policy: str
        _json_serializer: Callable[[Any], str]
        _json_deserializer: Callable[[str], Any]

    def _artifact_table_ddl(self) -> "list[str]":
        ddl = f"""CREATE TABLE IF NOT EXISTS {self._artifact_table} (
    app_name STRING(128) NOT NULL,
    user_id STRING(128) NOT NULL,
    session_id STRING(128) NOT NULL,
    filename STRING(512) NOT NULL,
    version INT64 NOT NULL,
    mime_type STRING(256),
    canonical_uri STRING(2048) NOT NULL,
    custom_metadata JSON,
    created_at TIMESTAMP NOT NULL
) PRIMARY KEY (app_name, user_id, session_id, filename, version){self._artifact_row_deletion_policy}"""
        return [ddl]

    def _build_drop_artifact_table_sql(self) -> "list[str]":
        return [f"DROP TABLE {self._artifact_table}"]

    def _build_insert_artifact_statement(self, record: StoredArtifact) -> "tuple[str, dict[str, Any], dict[str, Any]]":
        normalized_session_id = record["session_id"] if record["session_id"] is not None else USER_SCOPED_SESSION_ID
        metadata = record.get("custom_metadata")
        sql = f"""INSERT INTO {self._artifact_table} (
    app_name,
    user_id,
    session_id,
    filename,
    version,
    mime_type,
    canonical_uri,
    custom_metadata,
    created_at
) VALUES (
    @app_name,
    @user_id,
    @session_id,
    @filename,
    @version,
    @mime_type,
    @canonical_uri,
    @custom_metadata,
    @created_at
)"""
        params: dict[str, Any] = {
            "app_name": record["app_name"],
            "user_id": record["user_id"],
            "session_id": normalized_session_id,
            "filename": record["filename"],
            "version": int(record["version"]),
            "mime_type": record["mime_type"],
            "canonical_uri": record["canonical_uri"],
            "custom_metadata": self._json_serializer(metadata) if metadata is not None else None,
            "created_at": record["created_at"],
        }
        types: dict[str, Any] = {
            "app_name": SPANNER_PARAM_TYPES.STRING,
            "user_id": SPANNER_PARAM_TYPES.STRING,
            "session_id": SPANNER_PARAM_TYPES.STRING,
            "filename": SPANNER_PARAM_TYPES.STRING,
            "version": SPANNER_PARAM_TYPES.INT64,
            "mime_type": SPANNER_PARAM_TYPES.STRING,
            "canonical_uri": SPANNER_PARAM_TYPES.STRING,
            "custom_metadata": _json_param_type(),
            "created_at": SPANNER_PARAM_TYPES.TIMESTAMP,
        }
        return (sql, params, types)

    def _build_get_artifact_query(
        self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None, version: "int | None" = None
    ) -> "tuple[str, dict[str, Any], dict[str, Any]]":
        normalized_session_id = session_id if session_id is not None else USER_SCOPED_SESSION_ID
        params: dict[str, Any] = {
            "app_name": app_name,
            "user_id": user_id,
            "session_id": normalized_session_id,
            "filename": filename,
        }
        types: dict[str, Any] = {
            "app_name": SPANNER_PARAM_TYPES.STRING,
            "user_id": SPANNER_PARAM_TYPES.STRING,
            "session_id": SPANNER_PARAM_TYPES.STRING,
            "filename": SPANNER_PARAM_TYPES.STRING,
        }
        if version is not None:
            params["version"] = int(version)
            types["version"] = SPANNER_PARAM_TYPES.INT64
            sql = f"""SELECT
    app_name,
    user_id,
    session_id,
    filename,
    version,
    mime_type,
    canonical_uri,
    custom_metadata,
    created_at
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id = @session_id
  AND filename = @filename
  AND version = @version
LIMIT 1"""
            return (sql, params, types)

        sql = f"""SELECT
    app_name,
    user_id,
    session_id,
    filename,
    version,
    mime_type,
    canonical_uri,
    custom_metadata,
    created_at
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id = @session_id
  AND filename = @filename
ORDER BY version DESC LIMIT 1"""
        return (sql, params, types)

    def _build_list_artifact_versions_query(
        self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None
    ) -> "tuple[str, dict[str, Any], dict[str, Any]]":
        normalized_session_id = session_id if session_id is not None else USER_SCOPED_SESSION_ID
        sql = f"""SELECT
    app_name,
    user_id,
    session_id,
    filename,
    version,
    mime_type,
    canonical_uri,
    custom_metadata,
    created_at
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id = @session_id
  AND filename = @filename
ORDER BY version ASC"""
        params: dict[str, Any] = {
            "app_name": app_name,
            "user_id": user_id,
            "session_id": normalized_session_id,
            "filename": filename,
        }
        types: dict[str, Any] = {
            "app_name": SPANNER_PARAM_TYPES.STRING,
            "user_id": SPANNER_PARAM_TYPES.STRING,
            "session_id": SPANNER_PARAM_TYPES.STRING,
            "filename": SPANNER_PARAM_TYPES.STRING,
        }
        return (sql, params, types)

    def _build_list_artifact_keys_query(
        self, app_name: str, user_id: str, session_id: "str | None" = None
    ) -> "tuple[str, dict[str, Any], dict[str, Any]]":
        params: dict[str, Any] = {"app_name": app_name, "user_id": user_id}
        types: dict[str, Any] = {"app_name": SPANNER_PARAM_TYPES.STRING, "user_id": SPANNER_PARAM_TYPES.STRING}
        if session_id is not None:
            params["session_id"] = session_id
            types["session_id"] = SPANNER_PARAM_TYPES.STRING
            sql = f"""SELECT DISTINCT filename
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id IN ('', @session_id)
ORDER BY filename ASC"""
            return (sql, params, types)

        sql = f"""SELECT DISTINCT filename
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id = ''
ORDER BY filename ASC"""
        return (sql, params, types)

    def _build_delete_artifact_statements(
        self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None
    ) -> "tuple[str, str, dict[str, Any], dict[str, Any]]":
        select_sql, params, types = self._build_list_artifact_versions_query(app_name, user_id, filename, session_id)
        delete_sql = (
            f"DELETE FROM {self._artifact_table} "
            "WHERE app_name = @app_name AND user_id = @user_id AND session_id = @session_id AND filename = @filename"
        )
        return (select_sql, delete_sql, params, types)

    def _build_get_next_version_query(
        self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None
    ) -> "tuple[str, dict[str, Any], dict[str, Any]]":
        normalized_session_id = session_id if session_id is not None else USER_SCOPED_SESSION_ID
        sql = f"""SELECT COALESCE(MAX(version) + 1, 0)
FROM {self._artifact_table}
WHERE app_name = @app_name
  AND user_id = @user_id
  AND session_id = @session_id
  AND filename = @filename"""
        params: dict[str, Any] = {
            "app_name": app_name,
            "user_id": user_id,
            "session_id": normalized_session_id,
            "filename": filename,
        }
        types: dict[str, Any] = {
            "app_name": SPANNER_PARAM_TYPES.STRING,
            "user_id": SPANNER_PARAM_TYPES.STRING,
            "session_id": SPANNER_PARAM_TYPES.STRING,
            "filename": SPANNER_PARAM_TYPES.STRING,
        }
        return (sql, params, types)

    def _build_delete_artifacts_older_than_statements(
        self, before: "datetime", app_name: "str | None" = None
    ) -> "tuple[str, str, dict[str, Any], dict[str, Any]]":
        clauses = ["created_at < @before"]
        params: dict[str, Any] = {"before": before}
        types: dict[str, Any] = {"before": SPANNER_PARAM_TYPES.TIMESTAMP}
        if app_name is not None:
            clauses.append("app_name = @app_name")
            params["app_name"] = app_name
            types["app_name"] = SPANNER_PARAM_TYPES.STRING
        where_sql = " AND ".join(clauses)
        select_sql = f"""SELECT
    app_name,
    user_id,
    session_id,
    filename,
    version,
    mime_type,
    canonical_uri,
    custom_metadata,
    created_at
FROM {self._artifact_table}
WHERE {where_sql}
ORDER BY app_name ASC, user_id ASC, session_id ASC, filename ASC, version ASC"""
        delete_sql = f"DELETE FROM {self._artifact_table} WHERE {where_sql}"
        return (select_sql, delete_sql, params, types)

    def _decode_metadata(self, value: Any) -> "dict[str, Any] | None":
        if value is None:
            return None
        if isinstance(value, str) and not value:
            return None
        decoded = _convert_json_row_value(value, json_deserializer=self._json_deserializer)
        if decoded is None:
            return None
        if isinstance(decoded, dict):
            return cast("dict[str, Any]", decoded)
        if isinstance(decoded, str):
            return cast("dict[str, Any]", self._json_deserializer(decoded)) if decoded else None
        return cast("dict[str, Any]", self._json_deserializer(str(decoded)))

    def _rows_to_artifacts(self, rows: "list[Any]") -> "list[StoredArtifact]":
        return [
            StoredArtifact(
                app_name=str(row[0]),
                user_id=str(row[1]),
                session_id=None if row[2] == USER_SCOPED_SESSION_ID else str(row[2]),
                filename=str(row[3]),
                version=int(row[4]),
                mime_type=str(row[5]) if row[5] is not None else None,
                canonical_uri=str(row[6]),
                custom_metadata=self._decode_metadata(row[7]),
                created_at=row[8],
            )
            for row in rows
        ]


[docs] class SpannerSyncADKArtifactStore(BaseSyncADKArtifactStore[SpannerSyncConfig], _SpannerADKArtifactStoreMixin): """Spanner ADK artifact store backed by synchronous Spanner client.""" __slots__ = ("_artifact_row_deletion_policy", "_json_deserializer", "_json_serializer") connector_name: ClassVar[str] = "spanner"
[docs] def __init__(self, config: SpannerSyncConfig) -> None: super().__init__(config) self._artifact_row_deletion_policy = _spanner_row_deletion_policy( _adk_config(config), "artifact_ttl_seconds", "created_at" ) self._json_serializer, self._json_deserializer = _extract_spanner_json_codecs(config)
[docs] def create_table(self) -> None: """Create the artifact versions table if it does not exist.""" if self._artifact_table in self._existing_tables(): return try: execute_ddl_sync(self._database(), self._artifact_table_ddl(), timeout=_DDL_TIMEOUT_SECONDS) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] def drop_table(self) -> None: """Drop the artifact versions table if it exists.""" if self._artifact_table not in self._existing_tables(): return try: execute_ddl_sync(self._database(), self._build_drop_artifact_table_sql(), timeout=_DDL_TIMEOUT_SECONDS) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] def insert_artifact(self, record: StoredArtifact) -> None: """Insert an artifact version metadata row.""" sql, params, types = self._build_insert_artifact_statement(record) try: cast("Any", self._database()).run_in_transaction(_SpannerSyncUpdateJob(sql, params, types)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] def get_artifact( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None, version: "int | None" = None ) -> "StoredArtifact | None": """Get a specific artifact version's metadata.""" sql, params, types = self._build_get_artifact_query(app_name, user_id, filename, session_id, version) try: rows = self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return None if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise if not rows: return None return self._rows_to_artifacts(rows)[0]
[docs] def list_artifact_versions( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None ) -> "list[StoredArtifact]": """List all version records for an artifact, ordered by version ascending.""" sql, params, types = self._build_list_artifact_versions_query(app_name, user_id, filename, session_id) try: rows = self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return [] if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise return self._rows_to_artifacts(rows)
[docs] def list_artifact_keys(self, app_name: str, user_id: str, session_id: "str | None" = None) -> "list[str]": """List distinct artifact filenames.""" sql, params, types = self._build_list_artifact_keys_query(app_name, user_id, session_id) try: rows = self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return [] if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise return [str(row[0]) for row in rows]
[docs] def delete_artifact( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None ) -> "list[StoredArtifact]": """Delete all version records for an artifact and return them.""" select_sql, delete_sql, params, types = self._build_delete_artifact_statements( app_name, user_id, filename, session_id ) job = _SpannerSyncArtifactDeleteJob(select_sql, delete_sql, params, types) try: rows = cast("list[Any]", cast("Any", self._database()).run_in_transaction(job)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc return self._rows_to_artifacts(rows)
[docs] def get_next_version(self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None) -> int: """Get the next version number for an artifact.""" sql, params, types = self._build_get_next_version_query(app_name, user_id, filename, session_id) try: rows = self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return 0 if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise if not rows or rows[0][0] is None: return 0 return int(rows[0][0])
[docs] def delete_artifacts_older_than(self, before: "datetime", app_name: "str | None" = None) -> "list[StoredArtifact]": """Delete artifact version rows created before a cutoff and return them.""" select_sql, delete_sql, params, types = self._build_delete_artifacts_older_than_statements(before, app_name) job = _SpannerSyncArtifactDeleteJob(select_sql, delete_sql, params, types) try: rows = cast("list[Any]", cast("Any", self._database()).run_in_transaction(job)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc return self._rows_to_artifacts(rows)
def _database(self) -> "SpannerDatabase": return self._config.get_database() def _existing_tables(self) -> "set[str]": try: return list_existing_table_names_sync(self._database()) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc def _run_read( self, sql: str, params: "dict[str, Any] | None" = None, types: "dict[str, Any] | None" = None ) -> "list[Any]": try: with self._config.provide_connection(transaction=False) as snapshot: reader = cast("_SpannerReadProtocol", snapshot) return list(reader.execute_sql(sql, params=params, param_types=types)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] class SpannerAsyncADKArtifactStore(BaseAsyncADKArtifactStore[SpannerAsyncConfig], _SpannerADKArtifactStoreMixin): """Spanner ADK artifact store backed by asynchronous Spanner client.""" __slots__ = ("_artifact_row_deletion_policy", "_json_deserializer", "_json_serializer") connector_name: ClassVar[str] = "spanner"
[docs] def __init__(self, config: SpannerAsyncConfig) -> None: super().__init__(config) self._artifact_row_deletion_policy = _spanner_row_deletion_policy( _adk_config(config), "artifact_ttl_seconds", "created_at" ) self._json_serializer, self._json_deserializer = _extract_spanner_json_codecs(config)
[docs] async def create_table(self) -> None: """Create the artifact versions table if it does not exist.""" if self._artifact_table in await self._existing_tables(): return try: await execute_ddl_async(await self._database(), self._artifact_table_ddl(), timeout=_DDL_TIMEOUT_SECONDS) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] async def drop_table(self) -> None: """Drop the artifact versions table if it exists.""" if self._artifact_table not in await self._existing_tables(): return try: await execute_ddl_async( await self._database(), self._build_drop_artifact_table_sql(), timeout=_DDL_TIMEOUT_SECONDS ) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] async def insert_artifact(self, record: StoredArtifact) -> None: """Insert an artifact version metadata row.""" sql, params, types = self._build_insert_artifact_statement(record) database = await self._database() try: await database.run_in_transaction(_SpannerAsyncUpdateJob(sql, params, types)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc
[docs] async def get_artifact( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None, version: "int | None" = None ) -> "StoredArtifact | None": """Get a specific artifact version's metadata.""" sql, params, types = self._build_get_artifact_query(app_name, user_id, filename, session_id, version) try: rows = await self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return None if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise if not rows: return None return self._rows_to_artifacts(rows)[0]
[docs] async def list_artifact_versions( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None ) -> "list[StoredArtifact]": """List all version records for an artifact, ordered by version ascending.""" sql, params, types = self._build_list_artifact_versions_query(app_name, user_id, filename, session_id) try: rows = await self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return [] if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise return self._rows_to_artifacts(rows)
[docs] async def list_artifact_keys(self, app_name: str, user_id: str, session_id: "str | None" = None) -> "list[str]": """List distinct artifact filenames.""" sql, params, types = self._build_list_artifact_keys_query(app_name, user_id, session_id) try: rows = await self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return [] if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise return [str(row[0]) for row in rows]
[docs] async def delete_artifact( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None ) -> "list[StoredArtifact]": """Delete all version records for an artifact and return them.""" select_sql, delete_sql, params, types = self._build_delete_artifact_statements( app_name, user_id, filename, session_id ) database = await self._database() job = _SpannerAsyncArtifactDeleteJob(select_sql, delete_sql, params, types) try: rows = cast("list[Any]", await database.run_in_transaction(job)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc return self._rows_to_artifacts(rows)
[docs] async def get_next_version( self, app_name: str, user_id: str, filename: str, session_id: "str | None" = None ) -> int: """Get the next version number for an artifact.""" sql, params, types = self._build_get_next_version_query(app_name, user_id, filename, session_id) try: rows = await self._run_read(sql, params, types) except (NotFound, SQLSpecError) as exc: if _is_spanner_table_missing(exc, self._artifact_table): return 0 if isinstance(exc, SpannerGoogleAPICallError): raise create_mapped_exception(exc) from exc raise if not rows or rows[0][0] is None: return 0 return int(rows[0][0])
[docs] async def delete_artifacts_older_than( self, before: "datetime", app_name: "str | None" = None ) -> "list[StoredArtifact]": """Delete artifact version rows created before a cutoff and return them.""" select_sql, delete_sql, params, types = self._build_delete_artifacts_older_than_statements(before, app_name) database = await self._database() job = _SpannerAsyncArtifactDeleteJob(select_sql, delete_sql, params, types) try: rows = cast("list[Any]", await database.run_in_transaction(job)) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc return self._rows_to_artifacts(rows)
async def _database(self) -> "SpannerAsyncDatabase": return await self._config.get_database() async def _existing_tables(self) -> "set[str]": try: return await list_existing_table_names_async(await self._database()) except SpannerGoogleAPICallError as exc: raise create_mapped_exception(exc) from exc async def _run_read( self, sql: str, params: "dict[str, Any] | None" = None, types: "dict[str, Any] | None" = None ) -> "list[Any]": return await _run_read_async(await self._database(), sql, params, types)
class _SpannerSyncArtifactDeleteJob: """Callable transaction work item for Spanner sync ADK artifact delete-and-return operations.""" __slots__ = ("_delete_sql", "_params", "_select_sql", "_types") def __init__(self, select_sql: str, delete_sql: str, params: "dict[str, Any]", types: "dict[str, Any]") -> None: self._select_sql = select_sql self._delete_sql = delete_sql self._params = params self._types = types def __call__(self, transaction: "SpannerTransaction") -> "list[Any]": rows = list( cast("_SpannerReadProtocol", transaction).execute_sql( self._select_sql, params=self._params, param_types=self._types ) ) if not rows: return [] cast("_SpannerWriteTransactionProtocol", transaction).execute_update( self._delete_sql, params=self._params, param_types=self._types ) return rows class _SpannerAsyncArtifactDeleteJob: """Callable async transaction work item for Spanner async ADK artifact delete-and-return operations.""" __slots__ = ("_delete_sql", "_params", "_select_sql", "_types") def __init__(self, select_sql: str, delete_sql: str, params: "dict[str, Any]", types: "dict[str, Any]") -> None: self._select_sql = select_sql self._delete_sql = delete_sql self._params = params self._types = types async def __call__(self, transaction: "SpannerAsyncTransaction") -> "list[Any]": result_set = await cast("Any", transaction).execute_sql( self._select_sql, params=self._params, param_types=self._types ) rows = [row async for row in result_set] if not rows: return [] await transaction.execute_update(self._delete_sql, params=self._params, param_types=self._types) return rows