"""Storage pipeline scaffolding for driver-aware storage bridge."""
from collections import deque
from functools import partial
from pathlib import Path
from time import perf_counter
from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast
from urllib.parse import unquote, urlparse
from mypy_extensions import mypyc_attr
from typing_extensions import NotRequired, TypedDict
from sqlspec.exceptions import ImproperConfigurationError, StorageCapabilityError
from sqlspec.storage._arrow_payload import StorageFormat, decode_arrow_payload, encode_arrow_payload
from sqlspec.storage._paths import FILE_PROTOCOL, FILE_SCHEME_PREFIX, strip_windows_drive_prefix
from sqlspec.storage.errors import execute_async_storage_operation, execute_sync_storage_operation
from sqlspec.storage.registry import StorageRegistry, storage_registry
from sqlspec.utils.serializers import get_serializer_metrics, serialize_collection, to_json
from sqlspec.utils.sync_tools import async_
from sqlspec.utils.type_guards import supports_async_read_bytes, supports_async_write_bytes
from sqlspec.utils.uuids import uuid4
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
from sqlspec.protocols import ObjectStoreProtocol
from sqlspec.typing import ArrowTable
__all__ = (
"AsyncStoragePipeline",
"PartitionStrategyConfig",
"ResolvedStorageTarget",
"StorageBridgeJob",
"StorageCapabilities",
"StorageDestination",
"StorageDiagnostics",
"StorageFormat",
"StorageTelemetry",
"SyncStoragePipeline",
"create_storage_bridge_job",
"get_recent_storage_events",
"get_storage_bridge_diagnostics",
"get_storage_bridge_metrics",
"record_storage_diagnostic_event",
"reset_storage_bridge_events",
"reset_storage_bridge_metrics",
)
StorageDestination: TypeAlias = str | Path
StorageDiagnostics: TypeAlias = dict[str, float]
[docs]
class StorageCapabilities(TypedDict):
"""Runtime-evaluated driver storage capabilities."""
arrow_export_enabled: bool
arrow_import_enabled: bool
parquet_export_enabled: bool
parquet_import_enabled: bool
partition_strategies: "list[str]"
default_storage_profile: NotRequired[str | None]
[docs]
class PartitionStrategyConfig(TypedDict, total=False):
"""Configuration for partition fan-out strategies."""
kind: str
partitions: int
rows_per_chunk: int
manifest_path: str
[docs]
class StorageTelemetry(TypedDict, total=False):
"""Telemetry payload for storage bridge operations."""
destination: str
bytes_processed: int
rows_processed: int
partitions_created: int
duration_s: float
format: str
extra: "dict[str, object]"
backend: str
correlation_id: str
config: str
bind_key: str
[docs]
class ResolvedStorageTarget(NamedTuple):
"""A storage destination resolved to an address and its backend protocol."""
uri: str
protocol: str
[docs]
class StorageBridgeJob(NamedTuple):
"""Handle representing a storage bridge operation."""
job_id: str
status: str
telemetry: StorageTelemetry
class _StorageBridgeMetrics:
__slots__ = ("bytes_written",)
def __init__(self) -> None:
self.bytes_written = 0
def record_bytes(self, count: int) -> None:
self.bytes_written += max(count, 0)
def snapshot(self) -> "dict[str, int]":
return {"storage_bridge.bytes_written": self.bytes_written}
def reset(self) -> None:
self.bytes_written = 0
_METRICS = _StorageBridgeMetrics()
_RECENT_STORAGE_EVENTS: "deque[StorageTelemetry]" = deque(maxlen=25)
_EMPTY_STORAGE_OPTIONS: dict[str, Any] = {}
_ARROW_WRITE_FORMATS = frozenset({"parquet", "arrow-ipc", "csv"})
_ROW_WRITE_FORMATS = frozenset({"json", "jsonl"})
def _storage_options(default_options: "dict[str, Any]", storage_options: "dict[str, Any] | None") -> "dict[str, Any]":
return default_options if storage_options is None else storage_options
def _csv_write_options_from_kwargs(storage_options: "dict[str, Any]") -> "dict[str, Any] | None":
return cast("dict[str, Any] | None", storage_options.get("write_options"))
def _csv_write_options(
format_choice: StorageFormat,
resolved_options: "dict[str, Any]",
default_options: "dict[str, Any]",
default_write_options: "dict[str, Any] | None",
) -> "dict[str, Any] | None":
if format_choice != "csv":
return None
if resolved_options is default_options:
return default_write_options
return _csv_write_options_from_kwargs(resolved_options)
[docs]
def get_storage_bridge_metrics() -> "dict[str, int]":
"""Return aggregated storage bridge metrics."""
return _METRICS.snapshot()
[docs]
def reset_storage_bridge_metrics() -> None:
"""Reset aggregated storage bridge metrics."""
_METRICS.reset()
def record_storage_diagnostic_event(telemetry: StorageTelemetry) -> None:
"""Record telemetry for inclusion in diagnostics snapshots."""
_RECENT_STORAGE_EVENTS.append(cast("StorageTelemetry", dict(telemetry)))
def get_recent_storage_events() -> "list[StorageTelemetry]":
"""Return recent storage telemetry events (most recent first)."""
return [cast("StorageTelemetry", dict(entry)) for entry in _RECENT_STORAGE_EVENTS]
def reset_storage_bridge_events() -> None:
"""Clear recorded storage telemetry events."""
_RECENT_STORAGE_EVENTS.clear()
[docs]
def create_storage_bridge_job(status: str, telemetry: StorageTelemetry) -> StorageBridgeJob:
"""Create a storage bridge job handle with a unique identifier."""
job = StorageBridgeJob(job_id=str(uuid4()), status=status, telemetry=telemetry)
record_storage_diagnostic_event(job.telemetry)
return job
[docs]
def get_storage_bridge_diagnostics() -> "StorageDiagnostics":
"""Return aggregated storage bridge + serializer cache metrics."""
diagnostics: dict[str, float] = {key: float(value) for key, value in get_storage_bridge_metrics().items()}
serializer_metrics = get_serializer_metrics()
for key, value in serializer_metrics.items():
diagnostics[f"serializer.{key}"] = float(value)
return diagnostics
def _encode_row_payload(rows: "list[Any]", format_hint: StorageFormat) -> bytes:
if format_hint == "json":
return to_json(rows, as_bytes=True)
buffer = bytearray()
for row in rows:
buffer.extend(to_json(row, as_bytes=True))
buffer.extend(b"\n")
return bytes(buffer)
def _validate_arrow_write_format(format_choice: StorageFormat) -> None:
"""Reject Arrow-table writes for formats that cannot carry an Arrow payload.
Args:
format_choice: Requested storage format.
Raises:
StorageCapabilityError: If the format is not an Arrow write format.
"""
if format_choice not in _ARROW_WRITE_FORMATS:
msg = "Arrow storage writes support only Parquet, Arrow IPC, and CSV formats"
raise StorageCapabilityError(
msg, capability="arrow_write", remediation="Write row payloads with the row storage APIs instead."
)
def _validate_row_write_format(format_choice: StorageFormat) -> None:
"""Reject row writes for formats that cannot carry a row payload.
Args:
format_choice: Requested storage format.
Raises:
StorageCapabilityError: If the format is not a row write format.
"""
if format_choice not in _ROW_WRITE_FORMATS:
msg = "Row storage writes support only JSON and JSONL formats"
raise StorageCapabilityError(
msg, capability="row_write", remediation="Write Arrow tables with the Arrow storage APIs instead."
)
def _encode_arrow_payload(
table: "ArrowTable",
format_choice: StorageFormat,
*,
compression: str | None,
write_options: "dict[str, Any] | None" = None,
) -> bytes:
return encode_arrow_payload(table, format_choice, compression=compression, write_options=write_options)
def _write_backend_sync(backend: "ObjectStoreProtocol", path: str, payload: bytes, *, backend_name: str) -> None:
execute_sync_storage_operation(
partial(backend.write_bytes_sync, path, payload), backend=backend_name, operation="write_bytes", path=path
)
def _read_backend_sync(backend: "ObjectStoreProtocol", path: str, *, backend_name: str) -> bytes:
return execute_sync_storage_operation(
partial(backend.read_bytes_sync, path), backend=backend_name, operation="read_bytes", path=path
)
def _decode_arrow_payload(payload: bytes, format_choice: StorageFormat) -> "ArrowTable":
return decode_arrow_payload(payload, format_choice)
def _resolve_alias_destination(
registry: StorageRegistry, destination: str, backend_options: "dict[str, Any]"
) -> "tuple[ObjectStoreProtocol, str, str] | None":
if not destination.startswith("alias://"):
return None
payload = destination.removeprefix("alias://")
alias_name, _, relative_path = payload.partition("/")
alias = alias_name.strip()
if not alias:
msg = "Alias destinations must include a registry alias before the path component"
raise ImproperConfigurationError(msg)
path_segment = relative_path.strip()
if not path_segment:
msg = "Alias destinations must include an object path after the alias name"
raise ImproperConfigurationError(msg)
backend = registry.get(alias, **backend_options)
return backend, path_segment.lstrip("/"), backend.backend_type
def _backend_path(destination: str) -> str:
if destination.startswith("file://"):
return destination.removeprefix("file://")
if "://" in destination:
_, remainder = destination.split("://", 1)
return remainder.lstrip("/")
return destination
def _storage_backend(
registry: StorageRegistry, destination: StorageDestination, backend_options: "dict[str, Any] | None"
) -> "tuple[ObjectStoreProtocol, str, str]":
destination_str = destination.as_posix() if isinstance(destination, Path) else str(destination)
options = _EMPTY_STORAGE_OPTIONS if backend_options is None else backend_options
alias_resolution = _resolve_alias_destination(registry, destination_str, options)
if alias_resolution is not None:
return alias_resolution
backend = registry.get(destination_str, **options)
normalized_path = _backend_path(destination_str)
return backend, normalized_path, backend.backend_type
def _backend_cache_key(destination: StorageDestination, backend_options: "dict[str, Any] | None") -> "str | None":
if backend_options:
return None
return destination.as_posix() if isinstance(destination, Path) else str(destination)
@mypyc_attr(allow_interpreted_subclasses=True)
class _StoragePipelineBase:
"""Shared registry and backend-resolution state for storage pipelines."""
__slots__ = ("_csv_write_options", "_resolved_backend_cache", "_storage_options", "registry")
def __init__(
self, *, registry: StorageRegistry | None = None, storage_options: "dict[str, Any] | None" = None
) -> None:
self.registry = registry or storage_registry
self._resolved_backend_cache: dict[str, tuple[ObjectStoreProtocol, str, str]] = {}
self._storage_options = _EMPTY_STORAGE_OPTIONS if storage_options is None else storage_options
self._csv_write_options = _csv_write_options_from_kwargs(self._storage_options)
def clear_cache(self) -> None:
"""Clear cached storage backend resolutions for this pipeline instance."""
self._resolved_backend_cache.clear()
def _backend(
self, destination: StorageDestination, backend_options: "dict[str, Any] | None"
) -> "tuple[ObjectStoreProtocol, str, str]":
"""Resolve storage backend and normalized path for a destination."""
cache_key = _backend_cache_key(destination, backend_options)
if cache_key is None:
return _storage_backend(self.registry, destination, backend_options)
cached = self._resolved_backend_cache.get(cache_key)
if cached is not None:
return cached
resolved = _storage_backend(self.registry, destination, backend_options)
self._resolved_backend_cache[cache_key] = resolved
return resolved
def resolve_destination(
self, destination: StorageDestination, storage_options: "dict[str, Any] | None" = None
) -> ResolvedStorageTarget:
"""Resolve a destination without opening a database session or reading an object.
Direct remote URIs retain their address. Alias paths resolve relative to
the registered backend. Local paths resolve to absolute filesystem paths
through the backend's path checks.
Args:
destination: Remote URI, local path, or ``alias://name/path``.
storage_options: Explicit backend options. Pipeline writer defaults
are not inherited by this method.
Returns:
The resolved address and backend protocol.
Raises:
ImproperConfigurationError: If the destination or alias is invalid.
StoragePathTraversalError: If the backend rejects the local path.
"""
backend, path, _backend_name = self._backend(destination, storage_options)
destination_str = str(destination)
if destination_str.startswith("alias://"):
uri = backend.resolve_uri(path)
elif backend.protocol == FILE_PROTOCOL:
if destination_str.startswith(FILE_SCHEME_PREFIX):
path = strip_windows_drive_prefix(unquote(urlparse(destination_str).path))
uri = backend.resolve_uri(Path(path).expanduser().resolve())
else:
uri = destination_str
return ResolvedStorageTarget(uri, backend.protocol)
[docs]
@mypyc_attr(allow_interpreted_subclasses=True)
class SyncStoragePipeline(_StoragePipelineBase):
"""Pipeline coordinating storage registry operations and telemetry."""
__slots__ = ()
[docs]
def write_rows(
self,
rows: "list[dict[str, Any]]",
destination: StorageDestination,
*,
format_hint: StorageFormat | None = None,
storage_options: "dict[str, Any] | None" = None,
) -> StorageTelemetry:
"""Write dictionary rows to storage using cached serializers."""
format_choice = format_hint or "jsonl"
_validate_row_write_format(format_choice)
serialized = serialize_collection(rows)
payload = _encode_row_payload(serialized, format_choice)
resolved_options = _storage_options(self._storage_options, storage_options)
return self._write_bytes(
payload, destination, rows=len(serialized), format_label=format_choice, storage_options=resolved_options
)
[docs]
def write_arrow(
self,
table: "ArrowTable",
destination: StorageDestination,
*,
format_hint: StorageFormat | None = None,
storage_options: "dict[str, Any] | None" = None,
compression: str | None = None,
) -> StorageTelemetry:
"""Write an Arrow table to storage using zero-copy buffers."""
format_choice = format_hint or "parquet"
_validate_arrow_write_format(format_choice)
resolved_options = _storage_options(self._storage_options, storage_options)
format_write_options = _csv_write_options(
format_choice, resolved_options, self._storage_options, self._csv_write_options
)
payload = _encode_arrow_payload(
table, format_choice, compression=compression, write_options=format_write_options
)
return self._write_bytes(
payload, destination, rows=int(table.num_rows), format_label=format_choice, storage_options=resolved_options
)
[docs]
def read_arrow(
self, source: StorageDestination, *, file_format: StorageFormat, storage_options: "dict[str, Any] | None" = None
) -> "tuple[ArrowTable, StorageTelemetry]":
"""Read an artifact from storage and decode it into an Arrow table."""
backend, path, backend_name = self._backend(source, storage_options)
payload = _read_backend_sync(backend, path, backend_name=backend_name)
table = _decode_arrow_payload(payload, file_format)
rows_processed = int(table.num_rows)
telemetry: StorageTelemetry = {
"destination": path,
"bytes_processed": len(payload),
"rows_processed": rows_processed,
"format": file_format,
"backend": backend_name,
}
return table, telemetry
[docs]
def stream_read(
self,
source: StorageDestination,
*,
chunk_size: int | None = None,
storage_options: "dict[str, Any] | None" = None,
) -> "Iterator[bytes]":
"""Stream bytes from an artifact."""
backend, path, _backend_name = self._backend(source, storage_options)
return backend.stream_read_sync(path, chunk_size=chunk_size)
def _write_bytes(
self,
payload: bytes,
destination: StorageDestination,
*,
rows: int,
format_label: str,
storage_options: "dict[str, Any]",
) -> StorageTelemetry:
backend, path, backend_name = self._backend(destination, storage_options)
start = perf_counter()
_write_backend_sync(backend, path, payload, backend_name=backend_name)
elapsed = perf_counter() - start
bytes_written = len(payload)
_METRICS.record_bytes(bytes_written)
telemetry: StorageTelemetry = {
"destination": path,
"bytes_processed": bytes_written,
"rows_processed": rows,
"duration_s": elapsed,
"format": format_label,
"backend": backend_name,
}
return telemetry
[docs]
@mypyc_attr(allow_interpreted_subclasses=True)
class AsyncStoragePipeline(_StoragePipelineBase):
"""Async variant of the storage pipeline leveraging async-capable backends when available."""
__slots__ = ()
async def write_rows(
self,
rows: "list[dict[str, Any]]",
destination: StorageDestination,
*,
format_hint: StorageFormat | None = None,
storage_options: "dict[str, Any] | None" = None,
) -> StorageTelemetry:
format_choice = format_hint or "jsonl"
_validate_row_write_format(format_choice)
serialized = serialize_collection(rows)
payload = await async_(_encode_row_payload)(serialized, format_choice)
resolved_options = _storage_options(self._storage_options, storage_options)
return await self._write_bytes_async(
payload, destination, rows=len(serialized), format_label=format_choice, storage_options=resolved_options
)
async def write_arrow(
self,
table: "ArrowTable",
destination: StorageDestination,
*,
format_hint: StorageFormat | None = None,
storage_options: "dict[str, Any] | None" = None,
compression: str | None = None,
) -> StorageTelemetry:
format_choice = format_hint or "parquet"
_validate_arrow_write_format(format_choice)
resolved_options = _storage_options(self._storage_options, storage_options)
format_write_options = _csv_write_options(
format_choice, resolved_options, self._storage_options, self._csv_write_options
)
payload = await async_(_encode_arrow_payload)(
table, format_choice, compression=compression, write_options=format_write_options
)
return await self._write_bytes_async(
payload, destination, rows=int(table.num_rows), format_label=format_choice, storage_options=resolved_options
)
async def _write_bytes_async(
self,
payload: bytes,
destination: StorageDestination,
*,
rows: int,
format_label: str,
storage_options: "dict[str, Any]",
) -> StorageTelemetry:
backend, path, backend_name = self._backend(destination, storage_options)
start = perf_counter()
if supports_async_write_bytes(backend):
await execute_async_storage_operation(
partial(backend.write_bytes_async, path, payload),
backend=backend_name,
operation="write_bytes",
path=path,
)
else:
await async_(_write_backend_sync)(backend=backend, path=path, payload=payload, backend_name=backend_name)
elapsed = perf_counter() - start
bytes_written = len(payload)
_METRICS.record_bytes(bytes_written)
telemetry: StorageTelemetry = {
"destination": path,
"bytes_processed": bytes_written,
"rows_processed": rows,
"duration_s": elapsed,
"format": format_label,
"backend": backend_name,
}
return telemetry
async def read_arrow_async(
self, source: StorageDestination, *, file_format: StorageFormat, storage_options: "dict[str, Any] | None" = None
) -> "tuple[ArrowTable, StorageTelemetry]":
backend, path, backend_name = self._backend(source, storage_options)
if supports_async_read_bytes(backend):
payload = await execute_async_storage_operation(
partial(backend.read_bytes_async, path), backend=backend_name, operation="read_bytes", path=path
)
else:
payload = await async_(_read_backend_sync)(backend=backend, path=path, backend_name=backend_name)
table = await async_(_decode_arrow_payload)(payload, file_format)
rows_processed = int(table.num_rows)
telemetry: StorageTelemetry = {
"destination": path,
"bytes_processed": len(payload),
"rows_processed": rows_processed,
"format": file_format,
"backend": backend_name,
}
return table, telemetry
[docs]
async def stream_read_async(
self,
source: StorageDestination,
*,
chunk_size: int | None = None,
storage_options: "dict[str, Any] | None" = None,
) -> "AsyncIterator[bytes]":
"""Stream bytes from an artifact asynchronously."""
backend, path, _backend_name = self._backend(source, storage_options)
return await backend.stream_read_async(path, chunk_size=chunk_size)