Source code for sqlspec.storage.backends.local

"""Local file system storage backend.

A simple, zero-dependency implementation for local file operations.
No external dependencies like fsspec or obstore required.
"""

import shutil
from collections.abc import AsyncIterator, Iterator
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload
from urllib.parse import unquote, urlparse

from mypy_extensions import mypyc_attr

from sqlspec.exceptions import FileNotFoundInStorageError
from sqlspec.storage._arrow_stream import iter_parquet_row_groups, validate_parquet_stream_options
from sqlspec.storage._paths import strip_windows_drive_prefix
from sqlspec.storage._utils import import_pyarrow_parquet
from sqlspec.storage.backends.base import AsyncArrowBatchIterator, AsyncThreadedBytesIterator
from sqlspec.storage.errors import execute_sync_storage_operation
from sqlspec.utils.sync_tools import async_

if TYPE_CHECKING:
    from sqlspec.typing import ArrowRecordBatch, ArrowTable

__all__ = ("LocalStore",)


@mypyc_attr(allow_interpreted_subclasses=True)
class LocalStore:
    """Simple local file system storage backend.

    Provides file system operations without requiring fsspec or obstore.
    Supports file:// URIs and regular file paths.

    All synchronous methods use the *_sync suffix for consistency with async methods.
    """

    __slots__ = ("base_path", "protocol")

    backend_type: ClassVar[str] = "local"

    def __init__(self, uri: str = "", **kwargs: Any) -> None:
        """Initialize local storage backend.

        Args:
            uri: File URI or path
            **kwargs: Additional options including:
            - base_path: Subdirectory relative to URI path. If relative, it's combined
                with the URI path. If absolute, it takes precedence (backward compatible).

            The URI may be a file:// path (Windows style like file:///C:/path is supported).
            When both URI and base_path are provided, they are combined:
                - file:///home/user/storage + base_path="subdir" -> /home/user/storage/subdir
                - file:///home/user/storage + base_path="/other" -> /other (absolute takes precedence)
        """
        if uri.startswith("file://"):
            parsed = urlparse(uri)
            path = strip_windows_drive_prefix(unquote(parsed.path))
            self.base_path = Path(path).resolve()
        elif uri:
            self.base_path = Path(uri).resolve()
        else:
            self.base_path = Path.cwd()

        if "base_path" in kwargs:
            # Combine URI path with base_path (Path division handles absolute paths correctly)
            # If base_path is absolute, it takes precedence (backward compatible)
            self.base_path = (self.base_path / kwargs["base_path"]).resolve()

        if not self.base_path.exists():
            self.base_path.mkdir(parents=True, exist_ok=True)
        elif self.base_path.is_file():
            self.base_path = self.base_path.parent

        self.protocol = "file"
def resolve_uri(self, path: "str | Path") -> str: """Resolve a backend-relative path to an absolute filesystem path. Args: path: The same backend-relative path accepted by read and write methods. Returns: The absolute filesystem path. The target does not need to exist. """ return str(self._resolve_path(path).resolve())