155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
import logging
|
|
import os
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple
|
|
|
|
from ray.data._internal.util import _check_import
|
|
from ray.data.block import BlockMetadata
|
|
from ray.data.datasource.datasource import Datasource, ReadTask
|
|
|
|
if TYPE_CHECKING:
|
|
from ray.data.context import DataContext
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HudiQueryType(Enum):
|
|
SNAPSHOT = "snapshot"
|
|
INCREMENTAL = "incremental"
|
|
|
|
@classmethod
|
|
def supported_types(cls) -> List[str]:
|
|
return [e.value for e in cls]
|
|
|
|
|
|
class HudiDatasource(Datasource):
|
|
"""Hudi datasource, for reading Apache Hudi table."""
|
|
|
|
def __init__(
|
|
self,
|
|
table_uri: str,
|
|
query_type: str,
|
|
filters: Optional[List[Tuple[str, str, str]]] = None,
|
|
hudi_options: Optional[Dict[str, str]] = None,
|
|
storage_options: Optional[Dict[str, str]] = None,
|
|
):
|
|
_check_import(self, module="hudi", package="hudi-python")
|
|
|
|
self._table_uri = table_uri
|
|
self._query_type = HudiQueryType(query_type.lower())
|
|
self._filters = filters or []
|
|
self._hudi_options = hudi_options or {}
|
|
self._storage_options = storage_options or {}
|
|
|
|
def get_read_tasks(
|
|
self,
|
|
parallelism: int,
|
|
per_task_row_limit: Optional[int] = None,
|
|
data_context: Optional["DataContext"] = None,
|
|
) -> List["ReadTask"]:
|
|
import numpy as np
|
|
import pyarrow
|
|
from hudi import HudiTableBuilder
|
|
|
|
def _perform_read(
|
|
table_uri: str,
|
|
base_file_paths: List[str],
|
|
options: Dict[str, str],
|
|
) -> Iterator["pyarrow.Table"]:
|
|
from hudi import HudiFileGroupReader
|
|
|
|
for p in base_file_paths:
|
|
file_group_reader = HudiFileGroupReader(table_uri, options)
|
|
batch = file_group_reader.read_file_slice_by_base_file_path(p)
|
|
yield pyarrow.Table.from_batches([batch])
|
|
|
|
hudi_table = (
|
|
HudiTableBuilder.from_base_uri(self._table_uri)
|
|
.with_hudi_options(self._hudi_options)
|
|
.with_storage_options(self._storage_options)
|
|
# Although hudi-rs supports MOR snapshot, we need to add an API in
|
|
# the next release to allow file group reader to take in a list of
|
|
# files. Hence, setting this config for now to restrain reading
|
|
# only on parquet files (read optimized mode).
|
|
# This won't affect reading COW.
|
|
.with_hudi_option("hoodie.read.use.read_optimized.mode", "true")
|
|
.build()
|
|
)
|
|
|
|
logger.info("Collecting file slices for Hudi table at: %s", self._table_uri)
|
|
|
|
if self._query_type == HudiQueryType.SNAPSHOT:
|
|
file_slices_splits = hudi_table.get_file_slices_splits(
|
|
parallelism, self._filters
|
|
)
|
|
elif self._query_type == HudiQueryType.INCREMENTAL:
|
|
start_ts = self._hudi_options.get("hoodie.read.file_group.start_timestamp")
|
|
end_ts = self._hudi_options.get("hoodie.read.file_group.end_timestamp")
|
|
# TODO(xushiyan): add table API to return splits of file slices
|
|
file_slices = hudi_table.get_file_slices_between(start_ts, end_ts)
|
|
file_slices_splits = np.array_split(file_slices, parallelism)
|
|
else:
|
|
raise ValueError(
|
|
f"Unsupported query type: {self._query_type}. Supported types are: {HudiQueryType.supported_types()}."
|
|
)
|
|
|
|
logger.info("Creating read tasks for Hudi table at: %s", self._table_uri)
|
|
|
|
reader_options = {
|
|
**hudi_table.storage_options(),
|
|
**hudi_table.hudi_options(),
|
|
}
|
|
|
|
schema = hudi_table.get_schema()
|
|
read_tasks = []
|
|
|
|
for file_slices_split in file_slices_splits:
|
|
num_rows = 0
|
|
relative_paths = []
|
|
input_files = []
|
|
size_bytes = 0
|
|
for file_slice in file_slices_split:
|
|
# A file slice in a Hudi table is a logical group of data files
|
|
# within a physical partition. Records stored in a file slice
|
|
# are associated with a commit on the Hudi table's timeline.
|
|
# For more info, see https://hudi.apache.org/docs/file_layouts
|
|
num_rows += file_slice.num_records
|
|
relative_path = file_slice.base_file_relative_path()
|
|
relative_paths.append(relative_path)
|
|
full_path = os.path.join(self._table_uri, relative_path)
|
|
input_files.append(full_path)
|
|
size_bytes += file_slice.base_file_size
|
|
|
|
if self._query_type == HudiQueryType.SNAPSHOT:
|
|
metadata = BlockMetadata(
|
|
num_rows=num_rows,
|
|
input_files=input_files,
|
|
size_bytes=size_bytes,
|
|
exec_stats=None,
|
|
)
|
|
elif self._query_type == HudiQueryType.INCREMENTAL:
|
|
# need the check due to
|
|
# https://github.com/apache/hudi-rs/issues/401
|
|
metadata = BlockMetadata(
|
|
num_rows=None,
|
|
input_files=input_files,
|
|
size_bytes=None,
|
|
exec_stats=None,
|
|
)
|
|
|
|
read_task = ReadTask(
|
|
read_fn=lambda paths=relative_paths: _perform_read(
|
|
self._table_uri, paths, reader_options
|
|
),
|
|
metadata=metadata,
|
|
schema=schema,
|
|
per_task_row_limit=per_task_row_limit,
|
|
)
|
|
read_tasks.append(read_task)
|
|
|
|
return read_tasks
|
|
|
|
def estimate_inmemory_data_size(self) -> Optional[int]:
|
|
# TODO(xushiyan) add APIs to provide estimated in-memory size
|
|
return None
|