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

161 lines
5.3 KiB
Python

import logging
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Union
import numpy as np
from ray._common.retry import call_with_retry
from ray.data._internal.util import _check_import
from ray.data.block import BlockMetadata
from ray.data.context import DataContext
from ray.data.datasource.datasource import Datasource, ReadTask
if TYPE_CHECKING:
import pyarrow
logger = logging.getLogger(__name__)
class LanceDatasource(Datasource):
"""Lance datasource, for reading Lance dataset."""
def __init__(
self,
uri: str,
version: Optional[Union[int, str]] = None,
columns: Optional[List[str]] = None,
filter: Optional[str] = None,
storage_options: Optional[Dict[str, str]] = None,
scanner_options: Optional[Dict[str, Any]] = None,
):
super().__init__()
_check_import(self, module="lance", package="pylance")
import lance
self._projection_map = None
self.uri = uri
self.scanner_options = scanner_options or {}
if columns is not None:
self.scanner_options["columns"] = columns
if filter is not None:
self.scanner_options["filter"] = filter
self.storage_options = storage_options
self.lance_ds = lance.dataset(
uri=uri, version=version, storage_options=storage_options
)
data_context = DataContext.get_current()
lance_config = data_context.lance_config
match = []
match.extend(lance_config.read_fragments_errors_to_retry)
match.extend(data_context.retried_io_errors)
self._retry_params = {
"description": "read lance fragments",
"match": match,
"max_attempts": lance_config.read_fragments_max_attempts,
"max_backoff_s": lance_config.read_fragments_retry_max_backoff_s,
}
def supports_predicate_pushdown(self) -> bool:
return True
def get_read_tasks(
self,
parallelism: int,
per_task_row_limit: Optional[int] = None,
data_context: Optional["DataContext"] = None,
) -> List[ReadTask]:
read_tasks = []
ds_fragments = self.scanner_options.get("fragments")
if ds_fragments is None:
ds_fragments = self.lance_ds.get_fragments()
# Lance scanner's filter attr accepts only a string (SQL).
# See: https://github.com/lance-format/lance/blob/aac74b441cdb6df7d78700dbba33c521e6379ca5/python/python/lance/lance/__init__.pyi#L230
filter_expr = (
str(self._predicate_expr.to_pyarrow())
if self._predicate_expr is not None
else None
)
filter_from_arg = self.scanner_options.get("filter")
if filter_from_arg is not None:
filter_expr = (
filter_from_arg
if filter_expr is None
else f"({filter_expr}) AND ({filter_from_arg})"
)
for fragments in np.array_split(ds_fragments, parallelism):
if len(fragments) <= 0:
continue
fragment_ids = [f.metadata.id for f in fragments]
num_rows = sum(f.count_rows() for f in fragments)
input_files = [
data_file.path() for f in fragments for data_file in f.data_files()
]
# TODO(chengsu): Take column projection into consideration for schema.
metadata = BlockMetadata(
num_rows=num_rows,
size_bytes=None,
input_files=input_files,
exec_stats=None,
)
# Use a copy per task to avoid mutation races when tasks run in parallel
task_scanner_options = dict(self.scanner_options)
if filter_expr is not None:
task_scanner_options["filter"] = filter_expr
lance_ds = self.lance_ds
retry_params = self._retry_params
read_task = ReadTask(
lambda f=fragment_ids, opts=task_scanner_options: _read_fragments_with_retry(
f,
lance_ds,
opts,
retry_params,
),
metadata,
schema=fragments[0].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(chengsu): Add memory size estimation to improve auto-tune of parallelism.
return None
def _read_fragments_with_retry(
fragment_ids,
lance_ds,
scanner_options,
retry_params,
) -> Iterator["pyarrow.Table"]:
return call_with_retry(
lambda: _read_fragments(fragment_ids, lance_ds, scanner_options),
**retry_params,
)
def _read_fragments(
fragment_ids,
lance_ds,
scanner_options,
) -> Iterator["pyarrow.Table"]:
"""Read Lance fragments in batches.
NOTE: Use fragment ids, instead of fragments as parameter, because pickling
LanceFragment is expensive.
"""
import pyarrow
fragments = [lance_ds.get_fragment(id) for id in fragment_ids]
scanner_options["fragments"] = fragments
scanner = lance_ds.scanner(**scanner_options)
for batch in scanner.to_reader():
yield pyarrow.Table.from_batches([batch])