| ################################################################################ |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| ################################################################################ |
| |
| """Blob I/O helpers for the Daft integration.""" |
| |
| from __future__ import annotations |
| |
| from functools import lru_cache |
| from typing import BinaryIO, Dict |
| |
| |
| @lru_cache(maxsize=16) |
| def _get_file_io(catalog_key: tuple, table: str): |
| from pypaimon.catalog.catalog_factory import CatalogFactory |
| catalog_options = dict(catalog_key) |
| return CatalogFactory.create(catalog_options).get_table(table).file_io |
| |
| |
| def _cache_key(catalog_options: Dict[str, str]) -> tuple: |
| return tuple(sorted(catalog_options.items())) |
| |
| |
| def _file_range_fields(): |
| from pypaimon.daft.daft_compat import file_range_position_field, file_range_size_field |
| return file_range_position_field(), file_range_size_field() |
| |
| |
| def _range_attr(file, name): |
| # Prefer _inner accessors: they read the embedded range; public File.size |
| # does a network stat (~200ms/blob). |
| inner = getattr(file, "_inner", None) |
| src = inner if inner is not None and hasattr(inner, name) else file |
| v = getattr(src, name) |
| return v() if callable(v) else v |
| |
| |
| def _resolve_file_range(file, pos_field=None, size_field=None): |
| if pos_field is None: |
| pos_field, size_field = _file_range_fields() |
| return (_range_attr(file, "path"), |
| _range_attr(file, pos_field), |
| _range_attr(file, size_field)) |
| |
| |
| def open_blob(file, catalog_options: Dict[str, str], table: str) -> BinaryIO: |
| """Open a Daft ``File`` as a seekable stream via pypaimon's FileIO.""" |
| from pypaimon.table.row.blob import OffsetInputStream |
| path, offset, length = _resolve_file_range(file) |
| fio = _get_file_io(_cache_key(catalog_options), table) |
| stream = fio.new_input_stream(path) |
| try: |
| return OffsetInputStream(stream, offset, length) |
| except Exception: |
| stream.close() |
| raise |
| |
| |
| def read_blob(column, catalog_options: Dict[str, str], table: str, *, max_concurrency: int = 64): |
| """Read a blob ``File`` column to ``binary`` bytes via concurrent ranged reads.""" |
| import daft |
| from daft import DataType |
| |
| @daft.func.batch(return_dtype=DataType.binary()) |
| def _read_blobs(files): |
| import pyarrow as pa |
| |
| pos_field, size_field = _file_range_fields() |
| # Vectorized field extraction: per-File attr access (to_pylist + |
| # f.path/.position/.size) is GIL-bound and serializes the pool. daft |
| # File is an arrow ExtensionArray; .storage holds url/offset/size. |
| try: |
| struct = files.to_arrow() |
| struct = getattr(struct, "storage", struct) |
| ranges = list(zip(struct.field("url").to_pylist(), |
| struct.field(pos_field).to_pylist(), |
| struct.field(size_field).to_pylist())) |
| except (AttributeError, KeyError, TypeError, ValueError): |
| # Unexpected column layout: fall back to per-File resolution (cheap). |
| ranges = [None if f is None else _resolve_file_range(f, pos_field, size_field) |
| for f in files.to_pylist()] |
| |
| fio = _get_file_io(_cache_key(catalog_options), table) |
| # Build Arrow from shared views without intermediate BLOB copies. |
| views = fio.read_ranges_coalesced_views(ranges, max_concurrency) |
| return pa.array(views, type=pa.binary()) |
| |
| return _read_blobs(column) |