192 lines
6.3 KiB
Python
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
|