blob: 67edfbd02530e9a7e177d9ba35661281cca5b374 [file]
# 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.
from typing import Any, List, Optional, Tuple
import pyarrow as pa
import pyarrow.dataset as ds
from pyarrow import RecordBatch
from pypaimon.common.file_io import FileIO
from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader
from pypaimon.read.reader.lance_utils import to_lance_specified
from pypaimon.schema.data_types import DataField, PyarrowFieldParser
from pypaimon.table.special_fields import SpecialFields
class FormatLanceReader(RecordBatchReader):
"""
A Format Reader that reads record batch from a Lance file using PyArrow,
and filters it based on the provided predicate and projection.
"""
# row_indices: from IndexedSplit (ANN vector search), discrete local row offsets within the file.
# shard_range: from SlicedSplit (parallel shard scan), a contiguous [start, end) row range within the file.
def __init__(self, file_io: FileIO, file_path: str, read_fields: List[DataField],
push_down_predicate: Any, batch_size: int = 1024,
row_indices: Optional[List[int]] = None,
shard_range: Optional[Tuple[int, int]] = None):
"""Initialize Lance reader."""
import lance
self._read_field_names = [f.name for f in read_fields]
file_path_for_lance, storage_options = to_lance_specified(file_io, file_path)
# Read file metadata (footer only) to get actual schema
file_schema_names = set(
lance.file.LanceFileReader(
file_path_for_lance, storage_options=storage_options
).metadata().schema.names
)
self.existing_fields = [f.name for f in read_fields
if f.name in file_schema_names]
self.missing_fields = [f.name for f in read_fields
if f.name not in file_schema_names]
# Read data with column pruning
columns_for_lance = self.existing_fields if self.existing_fields else None
lance_reader = lance.file.LanceFileReader(
file_path_for_lance,
storage_options=storage_options,
columns=columns_for_lance)
if row_indices is not None:
reader_results = lance_reader.take_rows(row_indices)
elif shard_range is not None:
reader_results = lance_reader.read_range(shard_range[0], shard_range[1] - shard_range[0])
else:
reader_results = lance_reader.read_all()
pa_table = reader_results.to_table()
# Precompute output schema for missing fields
if self.missing_fields:
output_schema = PyarrowFieldParser.from_paimon_schema(read_fields)
self._missing_out_fields = []
for name in self.missing_fields:
idx = output_schema.get_field_index(name)
col_type = output_schema.field(idx).type if idx >= 0 else pa.null()
nullable = not SpecialFields.is_system_field(name)
self._missing_out_fields.append(pa.field(name, col_type, nullable=nullable))
if push_down_predicate is not None:
in_memory_dataset = ds.InMemoryDataset(pa_table)
scanner = in_memory_dataset.scanner(filter=push_down_predicate, batch_size=batch_size)
self.reader = scanner.to_reader()
else:
self.reader = iter(pa_table.to_batches(max_chunksize=batch_size))
def read_arrow_batch(self) -> Optional[RecordBatch]:
try:
# Handle both scanner reader and iterator
if hasattr(self.reader, 'read_next_batch'):
batch = self.reader.read_next_batch()
else:
batch = next(self.reader)
if not self.missing_fields:
return batch
# Reconstruct the batch with all fields in the correct order
all_columns = []
out_fields = []
for field_name in self._read_field_names:
if field_name in self.existing_fields:
col_idx = self.existing_fields.index(field_name)
all_columns.append(batch.column(col_idx))
out_fields.append(batch.schema.field(col_idx))
else:
miss_idx = self.missing_fields.index(field_name)
out_field = self._missing_out_fields[miss_idx]
all_columns.append(pa.nulls(batch.num_rows, type=out_field.type))
out_fields.append(out_field)
return pa.RecordBatch.from_arrays(all_columns, schema=pa.schema(out_fields))
except StopIteration:
return None
def close(self):
if self.reader is not None:
self.reader = None