Files
2026-07-13 13:17:40 +08:00

192 lines
6.3 KiB
Python

"""
DataSourceV2 API - Unified Abstraction for Reading Data Sources
This module defines a unified, extensible API for reading data from diverse sources
in Ray Data. The API provides a common abstraction layer that enables datasources to
declaratively expose their capabilities—such as filter pushdown, projection pruning,
and parallel reads—while allowing the execution engine to leverage these capabilities
transparently.
Core Principles:
- Modularity: Separate concerns (indexing, scanning, reading)
- Expressivity: Declarative capability exposure via mixins
- Extensibility: Easy to add new datasources with custom optimizations
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Generic,
Optional,
)
import pyarrow as pa
from ray.data._internal.datasource_v2 import InputSplit
from ray.data._internal.datasource_v2.listing.file_indexer import FileIndexer
from ray.util.annotations import DeveloperAPI
if TYPE_CHECKING:
from pyarrow.fs import FileSystem
from ray.data._internal.datasource_v2.readers.in_memory_size_estimator import (
InMemorySizeEstimator,
)
from ray.data._internal.datasource_v2.scanners.scanner import Scanner
@DeveloperAPI
class DatasourceCategory(Enum):
"""Categories of datasources with different capability profiles.
Each category has a distinct set of applicable optimizations:
- FILE_BASED: Local/cloud files (parquet, csv, json, images)
- DATABASE: SQL databases (postgres, mysql, snowflake)
- DATA_LAKE: Table formats (iceberg, delta, hudi)
- IN_MEMORY: In-process data (pandas, numpy, arrow)
- SYNTHETIC: Generated data (range, range_tensor)
- STREAMING: Unbounded sources (kafka, kinesis)
"""
FILE_BASED = "file_based"
DATABASE = "database"
DATA_LAKE = "data_lake"
IN_MEMORY = "in_memory"
SYNTHETIC = "synthetic"
STREAMING = "streaming"
@DeveloperAPI
class DataSourceV2(ABC, Generic[InputSplit]):
"""Abstract base class for V2 datasources.
DataSourceV2 is the entry point for reading data from a source. It provides:
1. File listing (for file-based sources) - via _get_file_indexer()
2. Schema inference
3. Size estimation
4. Scanner creation
Subclasses should implement the abstract methods and can optionally
override _get_file_indexer() and get_size_estimator() for file-based sources.
Example::
datasource = ParquetDatasourceV2()
indexer = datasource._get_file_indexer()
# List files with optional sampling
for manifest in indexer.list_files(paths, filesystem=fs):
schema = datasource.infer_schema(manifest)
break # Just need first manifest for schema
scanner = datasource.create_scanner(schema)
scanner = scanner.prune_columns(["col1", "col2"])
reader = scanner.create_reader()
for table in reader.read(manifest):
process(table)
"""
def __init__(self, name: str, category: DatasourceCategory):
"""Initialize the datasource.
Args:
name: Human-readable name for this datasource.
category: Category of this datasource.
"""
self._name = name
self._category = category
# File-based subclasses set this to ``False`` in their ``__init__``
# when the user-supplied paths are in the ``local://`` scheme —
# the driver node is the only one that can read those files.
# ``_read_datasource_v2`` consults the flag to decide whether to
# pin read tasks via a ``label_selector``.
self._supports_distributed_reads: bool = True
@property
def name(self) -> str:
"""Human-readable name for this datasource."""
return self._name
@property
def category(self) -> DatasourceCategory:
"""Category of this datasource."""
return self._category
@property
def supports_distributed_reads(self) -> bool:
"""Whether read tasks may run on any cluster node.
Defaults to ``True``. File-based subclasses (e.g.
:class:`ParquetDatasourceV2`) flip this to ``False`` when the
user supplies ``local://``-scheme paths so ``_read_datasource_v2``
can pin reads to the driver node via a ``ray.io/node-id``
label selector. Mirrors V1 ``Datasource.supports_distributed_reads``.
"""
return self._supports_distributed_reads
def _get_file_indexer(self) -> Optional[FileIndexer]:
"""Return FileIndexer component if applicable.
Override this for file-based datasources to provide file discovery.
Returns:
FileIndexer instance, or None for non-file-based sources.
"""
return None
def get_size_estimator(self) -> Optional[InMemorySizeEstimator]:
"""Return size estimator for this datasource.
Override this to provide format-specific size estimation.
Returns:
InMemorySizeEstimator instance, or None if not supported.
"""
return None
@abstractmethod
def infer_schema(self, sample: InputSplit) -> pa.Schema:
"""Infer schema from a sample of data.
Args:
sample: Sample data to infer schema from.
Returns:
PyArrow Schema inferred from the sample.
Raises:
ValueError: If schema cannot be inferred from the sample.
"""
...
@abstractmethod
def create_scanner(
self,
schema: pa.Schema,
filesystem: Optional["FileSystem"] = None,
**options: Any,
) -> Scanner[InputSplit]:
"""Create a Scanner for reading data.
Args:
schema: Schema for the data to read.
filesystem: Optional filesystem for file-based sources.
**options: Additional datasource-specific options.
Returns:
Configured Scanner instance.
"""
...
def resolve_partitioning(self, sample: InputSplit) -> Optional[Any]:
"""Return a partitioning descriptor derived from ``sample``, or ``None``.
Override this for file-based sources whose partition keys must be
discovered from a sample path (e.g. hive layouts where field names
are not known up front). The resolved descriptor is passed into
:meth:`create_scanner`.
"""
return None