blob: bfe592f456e368bd957ca9984c46a410633f9d69 [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.
import re
import struct
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, List, Optional
import pyarrow as pa
import pyarrow.dataset as ds
from pyarrow import RecordBatch
from pypaimon.common.file_io import FileIO, supports_pread, pread
from pypaimon.common.predicate import Predicate
from pypaimon.manifest.schema.simple_stats import SimpleStats
from pypaimon.read.push_down_utils import rewrite_predicate_indices
from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader
from pypaimon.schema.data_types import DataField, PyarrowFieldParser
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.table.special_fields import SpecialFields
class FormatMosaicReader(RecordBatchReader):
def __init__(self, file_io: FileIO, file_path: str, read_fields: List[DataField],
push_down_predicate: Any, batch_size: int = 1024,
row_group_predicate: Optional[Predicate] = None):
from mosaic import MosaicReader
self._read_fields = read_fields
self._read_field_names = [f.name for f in read_fields]
self._batch_size = batch_size
stream = file_io.new_input_stream(file_path)
file_length = file_io.get_file_size(file_path)
if supports_pread(stream):
self._stream = stream
def read_at(offset, length):
return pread(stream, length, offset)
else:
self._stream = None
file_data = stream.read()
stream.close()
file_length = len(file_data)
def read_at(offset, length):
return file_data[offset:offset + length]
self._reader = MosaicReader.from_input_file(read_at, file_length)
file_schema_names = set(f.name for f in self._reader.schema)
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]
if self.existing_fields:
self._reader.project(self.existing_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))
self._current_rg = 0
self._num_row_groups = self._reader.num_row_groups
self._current_batches = None
self._predicate = push_down_predicate
self._row_group_predicate = None
if row_group_predicate is not None:
try:
self._row_group_predicate = rewrite_predicate_indices(
row_group_predicate, read_fields)
except ValueError:
pass
def _next_row_group_batches(self):
while self._current_rg < self._num_row_groups:
if not self._matches_row_group(self._current_rg):
self._current_rg += 1
continue
batch = self._reader.read_row_group(self._current_rg)
self._current_rg += 1
if batch.num_rows == 0:
continue
batch = self._fill_missing_fields(batch)
if self._predicate is not None:
dataset = ds.InMemoryDataset(pa.Table.from_batches([batch]))
scanner = dataset.scanner(filter=self._predicate, batch_size=self._batch_size)
return scanner.to_reader()
else:
return iter(pa.Table.from_batches([batch]).to_batches(
max_chunksize=self._batch_size))
return None
def _matches_row_group(self, row_group_index: int) -> bool:
if self._row_group_predicate is None:
return True
try:
row_count = self._reader.row_group_num_rows(row_group_index)
stats_map = self._reader.get_row_group_statistics(row_group_index)
except Exception:
return True
if not stats_map:
return True
try:
stats = self._to_simple_stats(stats_map)
return self._row_group_predicate.test_by_simple_stats(stats, row_count)
except Exception:
return True
def _to_simple_stats(self, stats_map) -> SimpleStats:
min_values = [None] * len(self._read_fields)
max_values = [None] * len(self._read_fields)
null_counts = [None] * len(self._read_fields)
for i, field in enumerate(self._read_fields):
stats = stats_map.get(field.name)
if stats is None:
continue
null_counts[i] = stats.null_count
if stats.has_min_max:
min_values[i] = _convert_stats_value(stats.min, field.type)
max_values[i] = _convert_stats_value(
stats.max, field.type, round_up_timestamp=True)
return SimpleStats(
GenericRow(min_values, self._read_fields),
GenericRow(max_values, self._read_fields),
null_counts)
def _fill_missing_fields(self, batch: RecordBatch) -> RecordBatch:
if not self.missing_fields:
return batch
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))
def read_arrow_batch(self) -> Optional[RecordBatch]:
while True:
if self._current_batches is not None:
try:
if hasattr(self._current_batches, 'read_next_batch'):
return self._current_batches.read_next_batch()
else:
return next(self._current_batches)
except StopIteration:
self._current_batches = None
self._current_batches = self._next_row_group_batches()
if self._current_batches is None:
return None
def close(self):
if self._stream is not None:
self._stream.close()
self._stream = None
self._reader = None
self._current_batches = None
_EPOCH = datetime(1970, 1, 1)
def _convert_stats_value(
value: Optional[bytes], data_type, round_up_timestamp: bool = False) -> Any:
if value is None:
return None
type_name = str(data_type).upper().strip()
type_name = re.sub(r"\s+NOT\s+NULL$", "", type_name)
type_name = re.sub(r"\s+NULL$", "", type_name)
base_type = type_name.split("(", 1)[0].strip()
if base_type in ("CHAR", "VARCHAR", "STRING"):
return value.decode("utf-8")
if base_type in ("BINARY", "VARBINARY", "BYTES", "BLOB"):
return value
if len(value) == 0:
return None
if base_type == "BOOLEAN":
return value[0] != 0
if base_type == "TINYINT":
return struct.unpack(">b", value[:1])[0]
if base_type == "SMALLINT":
return struct.unpack(">h", value)[0]
if base_type in ("INT", "INTEGER", "DATE", "TIME"):
return struct.unpack(">i", value)[0]
if base_type == "BIGINT":
return struct.unpack(">q", value)[0]
if base_type == "FLOAT":
return struct.unpack(">f", value)[0]
if base_type == "DOUBLE":
return struct.unpack(">d", value)[0]
if base_type in ("DECIMAL", "NUMERIC"):
scale = _type_scale(type_name)
return Decimal(int.from_bytes(value, byteorder="big", signed=True)).scaleb(-scale)
if base_type in ("TIMESTAMP", "TIMESTAMP_LTZ"):
precision = _type_precision(type_name, default=6)
if precision <= 3:
return _EPOCH + timedelta(milliseconds=struct.unpack(">q", value)[0])
if precision <= 6:
return _EPOCH + timedelta(microseconds=struct.unpack(">q", value)[0])
millis, nanos_of_milli = struct.unpack(">qi", value)
microseconds = nanos_of_milli // 1000
if round_up_timestamp and nanos_of_milli % 1000 != 0:
microseconds += 1
return _EPOCH + timedelta(
milliseconds=millis,
microseconds=microseconds)
return None
def _type_precision(type_name: str, default: int = 0) -> int:
params = _type_params(type_name)
if not params:
return default
return int(params[0])
def _type_scale(type_name: str) -> int:
params = _type_params(type_name)
if len(params) < 2:
return 0
return int(params[1])
def _type_params(type_name: str) -> List[str]:
match = re.search(r"\(([^)]*)\)", type_name)
if match is None:
return []
return [p.strip() for p in match.group(1).split(",") if p.strip()]