blob: 5128bc7dcc03f102a4c1ce82336161b5ce143fb9 [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.
"""Native reads and adaptation to the public Arrow and row contracts."""
from typing import List, Optional
import pyarrow
from pypaimon.schema.arrow_schema import read_arrow_schema
from pypaimon.read.read_support import close_read_source
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.predicate import Predicate
from pypaimon.execution.native import runtime
from pypaimon.execution.native import table as native_table
from pypaimon.execution.native.read import builder as native_builder
from pypaimon.read.read_type import OutputProjection, adapter_output_paths, extract_array, output_schema
from pypaimon.read.split import Split
from pypaimon.schema.data_types import DataField
_NATIVE_READ_FILE_FORMATS = frozenset({
CoreOptions.FILE_FORMAT_PARQUET, CoreOptions.FILE_FORMAT_ORC,
CoreOptions.FILE_FORMAT_AVRO, CoreOptions.FILE_FORMAT_ROW,
CoreOptions.FILE_FORMAT_MOSAIC,
})
_NATIVE_READ_FILE_SUFFIXES = tuple('.%s' % file_format for file_format in _NATIVE_READ_FILE_FORMATS)
_NATIVE_BLOB_FILE_SUFFIXES = ('.blob', '.video')
def _prepare_native_read(table, predicate: Optional[Predicate] = None,
limit: Optional[int] = None,
projection: Optional[List[str]] = None,
blob_parallelism: Optional[int] = None,
read_type: Optional[List[DataField]] = None,
include_row_kind: bool = False):
"""Create the configured Core reader entrypoint."""
if not runtime.native_runtime_available():
raise RuntimeError(
"read.native.enabled needs the pypaimon-rust extension")
if not native_table._native_query_auth_supported(table):
raise RuntimeError('Native reads cannot reconstruct custom query authorization')
if read_type is None:
from pypaimon.read.read_builder import ReadBuilder
resolved = ReadBuilder(table)
if projection is not None:
resolved.with_projection(projection)
read_type = resolved.read_type()
builder = native_builder._configure_native_read_builder(
native_builder._native_read_builder(table), predicate, limit, projection,
read_type=read_type,
include_row_kind=include_row_kind)
if blob_parallelism is not None:
builder = builder.with_blob_parallelism(blob_parallelism)
reader = builder.new_read()
return reader.read_arrow
def native_read(table, splits, predicate: Optional[Predicate] = None,
limit: Optional[int] = None,
projection: Optional[List[str]] = None,
blob_parallelism: Optional[int] = None,
read_type: Optional[List[DataField]] = None,
include_row_kind: bool = False,
parallelism: int = 1,
ordered: bool = False):
"""Run one Core read; ``ordered`` retains the supplied split order."""
read_splits = _prepare_native_read(
table,
predicate=predicate,
limit=limit,
projection=projection,
blob_parallelism=blob_parallelism,
read_type=read_type,
include_row_kind=include_row_kind)
return read_splits(splits, parallelism=parallelism, ordered=ordered)
class NativeTableRead:
"""Adapt one configured read to Core without constructing Python readers."""
def __init__(self, read):
self._read = read
def read_batches(
self,
splits: List[Split],
schema: pyarrow.Schema,
parallelism: Optional[int] = None,
blob_parallelism=1,
ordered: bool = False):
"""Return Rust-read batches, or ``None`` when this read must fall back."""
read = self._read
if not read.table.options.native_read_enabled():
return self._fallback("read.native.enabled is false")
if read.table.options.file_format() not in _NATIVE_READ_FILE_FORMATS:
return self._fallback(
"the table file format is not supported")
if not splits:
return []
if not self._blob_view_supported():
return self._fallback(
"the table uses an unsupported BLOB view")
try:
from pypaimon.execution.native.read.split_adapter import native_split_from_python
except Exception as e:
return self._fallback(str(e), e, log=True)
rust_splits = []
for split in splits:
if not self._split_files_supported(split):
return self._fallback(
"a split contains a file unsupported by the native reader")
rust_split = getattr(split, '_native_split', None)
if rust_split is None:
try:
rust_split = native_split_from_python(split, ranges_use_row_ids=(
read.table.options.row_tracking_enabled() or read.table.options.data_evolution_enabled()))
except Exception as e:
return self._fallback(
"native split conversion failed: %s" % e,
e,
log=True)
rust_splits.append(rust_split)
# Python adapts the public configuration; Core owns split grouping,
# bounded concurrency, the row quota and cancellation of every stream.
effective = 1 if parallelism is None else parallelism
try:
batches = native_read(
read.table, rust_splits,
parallelism=effective,
ordered=ordered,
**self._read_kwargs(blob_parallelism))
except Exception as error:
runtime._raise_if_native_fork_safety_error(error)
return self._fallback(str(error), error, log=True)
return self.convert_batches(batches, schema)
def _fallback(self, reason, error=None, log=False):
from pypaimon.read.read_dispatch import native_read_fallback
return native_read_fallback(self._read, reason, error, log)
def _read_kwargs(self, blob_parallelism=None):
read = self._read
kwargs = {
'predicate': read.predicate,
'limit': read.limit,
}
if blob_parallelism is not None:
kwargs['blob_parallelism'] = blob_parallelism
kwargs['read_type'] = read.read_type
if read.include_row_kind:
kwargs['include_row_kind'] = True
return kwargs
@staticmethod
def _split_files_supported(split):
for data_file in split.files:
file_name = data_file.file_name.lower()
if (not file_name.endswith(_NATIVE_READ_FILE_SUFFIXES)
and not file_name.endswith(_NATIVE_BLOB_FILE_SUFFIXES)):
return False
return True
def convert_batches(self, batches, schema):
read = self._read
failure = None
try:
for batch in batches:
if batch.num_rows == 0:
continue
batch = self._prepare_batch(batch)
batch = read._project_batch_to_output(batch)
yield read._try_to_pad_batch_by_schema(batch, schema)
except BaseException as error:
failure = error
raise
finally:
close_read_source(batches, failure)
def _prepare_batch(self, batch):
read = self._read
if read.include_row_kind and 'rowkind' in batch.schema.names:
index = batch.schema.get_field_index('rowkind')
batch = batch.rename_columns([
read._row_kind_column if position == index else name
for position, name in enumerate(batch.schema.names)
])
if read.nested_name_paths:
batch = self._flatten_nested_batch(batch)
return batch
def _flatten_nested_batch(self, batch):
read = self._read
_, paths = read._reader_adapter()
# Rust already evaluated the complete predicate and projected away
# filter-only fields. Flatten just the user output dependencies; the
# Python split reader retains the additional adapter inputs internally.
outputs = adapter_output_paths(read.output_projection, read._adapter_read_type, paths)
needed = {path[0] for _, path in outputs}
fields_and_paths = [(field, path) for field, path in zip(read._adapter_read_type, paths)
if field.name in needed]
adapter_fields = [field for field, _ in fields_and_paths]
paths = [path for _, path in fields_and_paths]
arrays = [extract_array(batch, path) for path in paths]
fields = list(read_arrow_schema(adapter_fields))
# Authorization can mask a NOT NULL field. Keep the captured source
# nullability when flattening, including whole-parent/MAP projections.
projected = output_schema(batch.schema, OutputProjection([
(field.name, path) for field, path in zip(fields, paths)]))
fields = [field.with_nullable(field.nullable or projected[i].nullable) for i, field in enumerate(fields)]
if read.include_row_kind:
arrays.insert(0, batch.column(read._row_kind_column))
fields.insert(0, batch.schema.field(read._row_kind_column))
return pyarrow.RecordBatch.from_arrays(arrays, schema=pyarrow.schema(fields))
def _blob_view_supported(self) -> bool:
read = self._read
from pypaimon.execution.native.table import _native_blob_view_supported
return _native_blob_view_supported(read.table, (field.name for field in read._scan_read_type))
def read_rows(self, splits):
read = self._read
from copy import copy
from pypaimon.execution.native.read.row_iterator import native_rows
from pypaimon.read.read_type import output_fields
# InternalRow carries row kind separately from projected fields.
read = copy(read)
read.include_row_kind = True
read._row_kind_column = 'rowkind'
schema = read._add_row_kind_to_schema(read._output_arrow_schema(splits), read._row_kind_column)
fields = output_fields(read.read_type, read.output_projection)
batches = NativeTableRead(read).read_batches(splits, schema, parallelism=1, ordered=True)
if batches is None:
return None
def rows():
failure = None
try:
yield from native_rows(batches, read.table, fields)
except BaseException as error:
failure = error
raise
finally:
close_read_source(batches, failure)
return rows()