blob: c84a1383add7b28b027cec07f8152304705d09f8 [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 os
import sys
import threading
from collections import OrderedDict, deque
from concurrent.futures import Future
from typing import Any, Callable, Deque, Dict, Iterator, List, Optional, Set, Tuple
import pyarrow as pa
import pyarrow.dataset as ds
from pyarrow import RecordBatch
from pypaimon.common.file_io import FileIO
from pypaimon.common.options.config import CatalogOptions
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.data.variant_shredding import (
VariantSchema,
assemble_shredded_column,
build_variant_schema,
is_shredded_variant,
)
from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader
from pypaimon.schema.data_types import (
ArrayType,
AtomicType,
DataField,
MapType,
MultisetType,
PyarrowFieldParser,
RowType,
)
from pypaimon.table.special_fields import SpecialFields
_DEFAULT_FILE_FORMAT_METADATA_CACHE_MAX_SIZE = 50 * 1024 * 1024
_FILE_FORMAT_METADATA_CACHE_MAX_ENTRIES = 4096
_FILE_FORMAT_METADATA_CACHE_MIN_ENTRY_SIZE = 8 * 1024
_FILE_FORMAT_METADATA_CACHE_CONTAINER_OVERHEAD = 256
class _FilesystemIdentity:
def __init__(self, filesystem):
self.filesystem = filesystem
def __hash__(self):
return id(self.filesystem)
def __eq__(self, other):
return (
isinstance(other, _FilesystemIdentity)
and self.filesystem is other.filesystem
)
class _FileFormatDatasetCache:
def __init__(
self,
max_size: int,
max_entries: int = _FILE_FORMAT_METADATA_CACHE_MAX_ENTRIES):
self.max_size = max_size
self.max_entries = max_entries
self.estimated_size = 0
self._entries = OrderedDict()
self._loads = {}
self._lock = threading.Lock()
def get_or_load(self, key: Tuple[Any, str, str], loader: Callable[[], Any],
size_estimator: Callable[[Any], Optional[int]]):
with self._lock:
entry = self._entries.get(key)
if entry is not None:
self._entries.move_to_end(key)
return entry[0]
future = self._loads.get(key)
if future is None:
future = Future()
self._loads[key] = future
should_load = True
else:
should_load = False
if not should_load:
return future.result()
try:
dataset = loader()
estimated_size = size_estimator(dataset)
except BaseException as exception:
future.set_exception(exception)
with self._lock:
self._loads.pop(key, None)
raise
with self._lock:
if estimated_size is not None:
estimated_size = max(1, estimated_size)
self._entries[key] = (dataset, estimated_size)
self.estimated_size += estimated_size
self._entries.move_to_end(key)
self._evict()
future.set_result(dataset)
with self._lock:
self._loads.pop(key, None)
return dataset
def resize(self, max_size: int):
with self._lock:
self.max_size = max_size
self._evict()
def _evict(self):
while (
self.estimated_size > self.max_size
or len(self._entries) > self.max_entries):
_, (_, evicted_size) = self._entries.popitem(last=False)
self.estimated_size -= evicted_size
_FILE_FORMAT_DATASET_CACHE = None
_FILE_FORMAT_DATASET_CACHE_LOCK = threading.Lock()
_FILE_FORMAT_DATASET_CACHE_PID = os.getpid()
def _ensure_file_format_dataset_cache_process():
global _FILE_FORMAT_DATASET_CACHE
global _FILE_FORMAT_DATASET_CACHE_LOCK
global _FILE_FORMAT_DATASET_CACHE_PID
current_pid = os.getpid()
if current_pid != _FILE_FORMAT_DATASET_CACHE_PID:
_FILE_FORMAT_DATASET_CACHE = None
_FILE_FORMAT_DATASET_CACHE_LOCK = threading.Lock()
_FILE_FORMAT_DATASET_CACHE_PID = current_pid
def _file_format_dataset_cache(max_size: int) -> _FileFormatDatasetCache:
global _FILE_FORMAT_DATASET_CACHE
_ensure_file_format_dataset_cache_process()
with _FILE_FORMAT_DATASET_CACHE_LOCK:
if _FILE_FORMAT_DATASET_CACHE is None:
_FILE_FORMAT_DATASET_CACHE = _FileFormatDatasetCache(max_size)
else:
_FILE_FORMAT_DATASET_CACHE.resize(max_size)
return _FILE_FORMAT_DATASET_CACHE
def _reset_file_format_dataset_cache():
global _FILE_FORMAT_DATASET_CACHE
_ensure_file_format_dataset_cache_process()
with _FILE_FORMAT_DATASET_CACHE_LOCK:
_FILE_FORMAT_DATASET_CACHE = None
def _estimate_file_format_dataset_size(dataset, file_format: str) -> Optional[int]:
try:
if file_format == 'parquet':
footer_size = 0
for fragment in dataset.get_fragments():
metadata = fragment.metadata
if metadata is not None:
footer_size += int(metadata.serialized_size)
if footer_size > 0:
return footer_size
return int(dataset.schema.serialize().size)
except Exception:
return None
def _estimate_file_format_cache_entry_size(
key: Tuple[Any, str, str],
dataset,
file_format: str) -> Optional[int]:
metadata_size = _estimate_file_format_dataset_size(dataset, file_format)
if metadata_size is None:
return None
# PyArrow does not expose the native size retained by Dataset and Fragment
# objects. Account for all visible Python objects and apply a conservative
# floor so tiny files cannot turn a byte-bounded cache into an effectively
# unbounded object cache.
visible_size = (
metadata_size
+ sys.getsizeof(key)
+ sys.getsizeof(key[0])
+ sys.getsizeof(key[1])
+ sys.getsizeof(key[2])
+ sys.getsizeof(dataset)
+ sys.getsizeof((dataset, metadata_size))
+ _FILE_FORMAT_METADATA_CACHE_CONTAINER_OVERHEAD
)
return max(_FILE_FORMAT_METADATA_CACHE_MIN_ENTRY_SIZE, visible_size)
def _file_format_metadata_cache_max_size(file_io: FileIO) -> int:
properties = getattr(file_io, 'properties', None)
if properties is None:
return _DEFAULT_FILE_FORMAT_METADATA_CACHE_MAX_SIZE
return properties.get(
CatalogOptions.FILE_FORMAT_METADATA_CACHE_MAX_SIZE).get_bytes()
def _file_format_dataset(file_io: FileIO, file_format: str, file_path: str,
cache_max_size: int):
file_path_for_pyarrow = file_io.to_filesystem_path(file_path)
filesystem = file_io.filesystem
def load():
return ds.dataset(
file_path_for_pyarrow, format=file_format, filesystem=filesystem)
key = (_FilesystemIdentity(filesystem), file_format, file_path_for_pyarrow)
if cache_max_size <= 0:
_reset_file_format_dataset_cache()
return load()
return _file_format_dataset_cache(cache_max_size).get_or_load(
key,
load,
lambda dataset: _estimate_file_format_cache_entry_size(
key, dataset, file_format))
class FormatPyArrowReader(RecordBatchReader):
"""
A Format Reader that reads record batch from a Parquet or ORC file using PyArrow,
and filters it based on the provided predicate and projection.
When a VARIANT column is stored in the shredded Parquet format (a struct with
``metadata``, ``value``, and ``typed_value`` fields), this reader transparently
reconstructs the standard ``struct<value: binary, metadata: binary>`` representation.
"""
def __init__(self, file_io: FileIO, file_format: str, file_path: str,
read_fields: List[DataField],
push_down_predicate: Any, batch_size: int = 1024,
options: CoreOptions = None,
nested_name_paths: Optional[List[List[str]]] = None,
predicate_field_names: Optional[Set[str]] = None,
row_indices: Optional[List[int]] = None,
row_ranges: Optional[List[Tuple[int, int]]] = None):
self._predicate_field_names = predicate_field_names or set()
file_path_for_pyarrow = file_io.to_filesystem_path(file_path)
cache_max_size = _file_format_metadata_cache_max_size(file_io)
self.dataset = _file_format_dataset(
file_io, file_format, file_path, cache_max_size)
self._range_slicer = None
self._selected_parquet_row_groups = None
self._exhausted = False
if row_indices is not None and row_ranges is not None:
raise ValueError(
"row_indices and row_ranges cannot both be provided")
row_selection_supplied = (
row_indices is not None or row_ranges is not None)
if row_selection_supplied and file_format == 'parquet':
if push_down_predicate is not None:
raise ValueError(
"row selections cannot be combined with a scanner-level "
"push-down predicate because filtering shifts row "
"positions")
runs = (
_normalize_runs(row_ranges)
if row_ranges is not None
else _to_runs(row_indices)
)
if not runs:
self._exhausted = True
else:
fragment = next(iter(self.dataset.get_fragments()), None)
row_group_fragments = (
list(fragment.split_by_row_group())
if fragment is not None else [])
selected_infos = []
selected_ids = []
offset = 0
run_index = 0
for row_group_fragment in row_group_fragments:
row_group = row_group_fragment.row_groups[0]
row_count = row_group.num_rows
lower, upper = offset, offset + row_count - 1
while (
run_index < len(runs)
and runs[run_index][1] < lower):
run_index += 1
if (run_index < len(runs)
and runs[run_index][0] <= upper):
selected_infos.append((offset, row_count))
selected_ids.append(row_group.id)
offset += row_count
if not selected_ids:
self._exhausted = True
else:
self._selected_parquet_row_groups = selected_ids
self._range_slicer = _RowRunSlicer(
selected_infos, runs)
self._file_format = file_format
self.read_fields = read_fields
self._read_field_names = [f.name for f in read_fields]
if nested_name_paths is not None and len(nested_name_paths) != len(read_fields):
raise ValueError(
"nested_name_paths length {} does not match read_fields length {}".format(
len(nested_name_paths), len(read_fields)))
self._nested_name_paths = nested_name_paths
has_nested_path = bool(
nested_name_paths and any(len(p) > 1 for p in nested_name_paths))
self._has_nested_path = has_nested_path
file_schema = self.dataset.schema
if has_nested_path:
self.existing_fields = []
self.missing_fields = []
for f, path in zip(read_fields, nested_name_paths):
if _path_exists_in_arrow_schema(file_schema, path):
self.existing_fields.append(f.name)
else:
self.missing_fields.append(f.name)
else:
file_schema_names = set(file_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]
self._variant_shredding_enabled = (
options is None or options.variant_shredding_enabled())
self._variant_schema_cache: Dict[pa.DataType, VariantSchema] = {}
self._bounded_variant_read = (
self._file_format == 'parquet' and self._has_projected_variant())
if has_nested_path and not self._bounded_variant_read:
existing_set = set(self.existing_fields)
columns_dict = {}
for f, path in zip(read_fields, nested_name_paths):
if f.name in existing_set:
columns_dict[f.name] = ds.field(*path)
self._scan_columns = columns_dict
elif has_nested_path:
self._scan_columns = None
else:
# Only pass existing fields to PyArrow scanner to avoid errors
self._scan_columns = self.existing_fields
self._scan_filter = push_down_predicate
self._scan_batch_size = batch_size
self._output_schema = (
PyarrowFieldParser.from_paimon_schema(read_fields) if read_fields else None
)
# Read projected VARIANT columns in bounded batches.
self._parquet_file = None
if (self._bounded_variant_read
or self._selected_parquet_row_groups is not None):
import pyarrow.parquet as pq
# ParquetFile(filesystem=...) is unavailable in PyArrow 6.
self._parquet_file = pq.ParquetFile(
file_io.filesystem.open_input_file(file_path_for_pyarrow))
if self._exhausted:
self._raw_batches = iter(())
elif self._parquet_file is not None:
self._raw_batches = self._iter_row_group_batches()
else:
reader = self.dataset.scanner(
columns=self._scan_columns,
filter=self._scan_filter,
batch_size=self._scan_batch_size,
).to_reader()
self._raw_batches = self._iter_reader_batches(reader)
def _has_projected_variant(self) -> bool:
return any(
f.name in self.existing_fields
and _contains_variant(f.type)
for f in self.read_fields)
@staticmethod
def _iter_reader_batches(reader):
while True:
try:
yield reader.read_next_batch()
except StopIteration:
return
def _iter_row_group_batches(self):
columns = self._row_group_read_columns()
for row_group in self._surviving_row_group_ids():
for batch in self._parquet_file.iter_batches(
row_groups=[row_group],
columns=columns,
batch_size=self._scan_batch_size):
if self._has_nested_path:
batches = [batch]
if self._scan_filter is not None:
table = ds.dataset(
pa.Table.from_batches([batch])
).scanner(filter=self._scan_filter).to_table()
batches = table.to_batches()
for filtered in batches:
out = self._select_nested_fields(filtered)
if out.num_rows:
yield out
continue
if self._scan_filter is None:
yield self._select_existing_fields(batch)
continue
table = ds.dataset(
pa.Table.from_batches([batch])
).scanner(filter=self._scan_filter).to_table()
if self.existing_fields:
table = table.select(self.existing_fields)
for out in table.to_batches():
if out.num_rows:
yield out
def _row_group_read_columns(self):
if self._has_nested_path:
existing = set(self.existing_fields)
columns = []
for field, path in zip(self.read_fields, self._nested_name_paths):
if field.name in existing and path[0] not in columns:
columns.append(path[0])
else:
columns = list(self.existing_fields)
if self._scan_filter is not None:
file_names = set(self.dataset.schema.names)
for name in self._predicate_field_names:
if name in file_names and name not in columns:
columns.append(name)
return columns
def _select_existing_fields(self, batch):
if not self.existing_fields:
return _zero_column_batch(batch.num_rows)
columns = []
fields = []
for name in self.existing_fields:
index = batch.schema.get_field_index(name)
if index < 0:
raise KeyError("Field not found in batch: {}".format(name))
columns.append(batch.column(index))
fields.append(batch.schema.field(index))
return pa.RecordBatch.from_arrays(columns, schema=pa.schema(fields))
def _select_nested_fields(self, batch):
columns = []
names = []
existing = set(self.existing_fields)
for field, path in zip(self.read_fields, self._nested_name_paths):
if field.name not in existing:
continue
index = batch.schema.get_field_index(path[0])
if index < 0:
raise KeyError("Field not found in batch: {}".format(path[0]))
column = batch.column(index)
for name in path[1:]:
index = column.type.get_field_index(name)
column = column.flatten()[index]
columns.append(column)
names.append(field.name)
if not columns:
return _zero_column_batch(batch.num_rows)
return pa.RecordBatch.from_arrays(columns, names=names)
def _surviving_row_group_ids(self):
total = self._parquet_file.num_row_groups
if self._scan_filter is None:
if self._selected_parquet_row_groups is not None:
return self._selected_parquet_row_groups
return range(total)
try:
ids = set()
for fragment in self.dataset.get_fragments(
filter=self._scan_filter):
for row_group in fragment.split_by_row_group(
self._scan_filter):
ids.update(info.id for info in row_group.row_groups)
return sorted(ids)
except Exception:
return range(total)
def read_arrow_batch(self) -> Optional[RecordBatch]:
if self._range_slicer is not None:
batch = self._range_slicer.next_batch(self._raw_batches)
else:
batch = next(self._raw_batches, None)
if batch is None:
return None
return self._post_process_batch(batch)
def _post_process_batch(self, batch: RecordBatch) -> RecordBatch:
if self._file_format == 'orc' and self._output_schema is not None:
batch = self._cast_orc_time_columns(batch)
if self._variant_shredding_enabled:
batch = self._assemble_shredded_variants(batch)
if not self.missing_fields:
return batch
def _type_for_missing(name: str) -> pa.DataType:
if self._output_schema is not None:
idx = self._output_schema.get_field_index(name)
if idx >= 0:
return self._output_schema.field(idx).type
return pa.null()
missing_columns = [
pa.nulls(batch.num_rows, type=_type_for_missing(name))
for name in self.missing_fields
]
all_columns = []
out_fields = []
for field_name in self._read_field_names:
if field_name in self.existing_fields:
column_idx = self.existing_fields.index(field_name)
all_columns.append(batch.column(column_idx))
out_fields.append(batch.schema.field(column_idx))
else:
column_idx = self.missing_fields.index(field_name)
col_type = _type_for_missing(field_name)
all_columns.append(missing_columns[column_idx])
nullable = not SpecialFields.is_system_field(field_name)
out_fields.append(
pa.field(field_name, col_type, nullable=nullable))
return pa.RecordBatch.from_arrays(
all_columns, schema=pa.schema(out_fields))
def _assemble_shredded_variants(self, batch: pa.RecordBatch) -> pa.RecordBatch:
changed = False
columns = list(batch.columns)
fields = list(batch.schema)
logical_types = {field.name: field.type for field in self.read_fields}
for i, f in enumerate(fields):
logical_type = logical_types.get(f.name)
if logical_type is not None:
new_col, column_changed = _assemble_variant_column(
columns[i], logical_type, self._variant_schema_cache)
else:
new_col, column_changed = columns[i], False
if column_changed:
columns[i] = new_col
fields[i] = pa.field(f.name, new_col.type, nullable=f.nullable)
changed = True
if not changed:
return batch
return pa.RecordBatch.from_arrays(columns, schema=pa.schema(fields))
def _cast_orc_time_columns(self, batch):
"""Cast int32 TIME columns back to time32('ms') when reading ORC.
"""
columns = []
fields = []
changed = False
for i, name in enumerate(batch.schema.names):
col = batch.column(i)
idx = self._output_schema.get_field_index(name)
if idx >= 0 and pa.types.is_int32(col.type) \
and pa.types.is_time(self._output_schema.field(idx).type):
col = col.cast(self._output_schema.field(idx).type)
fields.append(self._output_schema.field(idx))
changed = True
else:
fields.append(batch.schema.field(i))
columns.append(col)
if changed:
return pa.RecordBatch.from_arrays(columns, schema=pa.schema(fields))
return batch
def close(self):
self._raw_batches = None
if self._parquet_file is not None:
close = getattr(self._parquet_file, 'close', None)
if close is not None:
close()
self._parquet_file = None
def _path_exists_in_arrow_schema(schema: pa.Schema, path: List[str]) -> bool:
"""Check whether a name path is fully resolvable in the given schema."""
if not path:
return False
if path[0] not in schema.names:
return False
current_type = schema.field(path[0]).type
for name in path[1:]:
if not pa.types.is_struct(current_type):
return False
idx = current_type.get_field_index(name)
if idx < 0:
return False
current_type = current_type[idx].type
return True
def _zero_column_batch(num_rows: int) -> RecordBatch:
"""Build a zero-column batch without losing its logical row count."""
empty_struct = pa.Array.from_buffers(
pa.struct([]), num_rows, [None], children=[])
return pa.RecordBatch.from_struct_array(empty_struct)
def _to_runs(row_indices: List[int]) -> List[Tuple[int, int]]:
"""Collapse row indices into sorted, distinct, inclusive runs."""
if not row_indices:
return []
sorted_indices = sorted(set(row_indices))
runs = []
start = previous = sorted_indices[0]
for index in sorted_indices[1:]:
if index == previous + 1:
previous = index
continue
runs.append((start, previous))
start = previous = index
runs.append((start, previous))
return runs
def _normalize_runs(
row_ranges: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""Sort and merge inclusive row ranges without expanding their rows."""
if not row_ranges:
return []
ranges = sorted(row_ranges)
merged = []
for lower, upper in ranges:
if lower > upper:
raise ValueError(
"Invalid row range: {} > {}".format(lower, upper))
if merged and lower <= merged[-1][1] + 1:
merged[-1] = (merged[-1][0], max(merged[-1][1], upper))
else:
merged.append((lower, upper))
return merged
class _RowRunSlicer:
"""Slice selected Parquet row groups down to requested file-local rows."""
def __init__(
self,
selected_infos: List[Tuple[int, int]],
runs: List[Tuple[int, int]]):
self._segments = []
concatenated_offset = 0
for file_offset, row_count in selected_infos:
self._segments.append((
concatenated_offset,
concatenated_offset + row_count,
file_offset,
))
concatenated_offset += row_count
self._runs = [(lower, upper + 1) for lower, upper in runs]
self._stream_offset = 0
self._segment_index = 0
self._run_index = 0
self._pending: Deque[RecordBatch] = deque()
def next_batch(
self, batches: Iterator[RecordBatch]) -> Optional[RecordBatch]:
while not self._pending:
batch = next(batches, None)
if batch is None:
return None
self._slice_batch(batch)
return self._pending.popleft()
def _slice_batch(self, batch: RecordBatch) -> None:
batch_start = self._stream_offset
batch_end = batch_start + batch.num_rows
self._stream_offset = batch_end
position = batch_start
while position < batch_end:
while (
self._segment_index < len(self._segments)
and position >= self._segments[
self._segment_index][1]):
self._segment_index += 1
if self._segment_index >= len(self._segments):
return
segment_start, segment_end, file_start = self._segments[
self._segment_index]
part_end = min(batch_end, segment_end)
local_start = file_start + position - segment_start
local_end = file_start + part_end - segment_start
while (
self._run_index < len(self._runs)
and self._runs[self._run_index][1] <= local_start):
self._run_index += 1
run_index = self._run_index
while (
run_index < len(self._runs)
and self._runs[run_index][0] < local_end):
run_start, run_end = self._runs[run_index]
lower = max(local_start, run_start)
upper = min(local_end, run_end)
if lower < upper:
offset = (
position - batch_start + lower - local_start)
self._pending.append(
batch.slice(offset, upper - lower))
if run_end <= local_end:
run_index += 1
else:
break
self._run_index = run_index
position = part_end
def _contains_variant(data_type) -> bool:
if isinstance(data_type, AtomicType):
return data_type.type.upper() == 'VARIANT'
if isinstance(data_type, (ArrayType, MultisetType)):
return _contains_variant(data_type.element)
if isinstance(data_type, MapType):
return (_contains_variant(data_type.key)
or _contains_variant(data_type.value))
if isinstance(data_type, RowType):
return any(_contains_variant(field.type) for field in data_type.fields)
return False
def _assemble_variant_column(column, data_type, schema_cache):
if isinstance(data_type, AtomicType):
if (data_type.type.upper() != 'VARIANT'
or not is_shredded_variant(column.type)):
return column, False
schema = schema_cache.get(column.type)
if schema is None:
schema = build_variant_schema(column.type)
schema_cache[column.type] = schema
return assemble_shredded_column(column, schema), True
if isinstance(data_type, RowType) and pa.types.is_struct(column.type):
logical_fields = {field.name: field.type for field in data_type.fields}
columns = []
fields = []
changed = False
for index, arrow_field in enumerate(column.type):
child = column.field(index)
logical_type = logical_fields.get(arrow_field.name)
if logical_type is not None:
child, child_changed = _assemble_variant_column(
child, logical_type, schema_cache)
changed = changed or child_changed
columns.append(child)
fields.append(pa.field(
arrow_field.name,
child.type,
nullable=arrow_field.nullable,
metadata=arrow_field.metadata,
))
if changed:
mask = column.is_null() if column.null_count else None
return pa.StructArray.from_arrays(
columns, fields=fields, mask=mask), True
return column, False
if (isinstance(data_type, (ArrayType, MultisetType))
and (pa.types.is_list(column.type)
or pa.types.is_large_list(column.type))):
offsets, start, end = _normalized_offsets(column)
values = column.values.slice(start, end - start)
values, changed = _assemble_variant_column(
values, data_type.element, schema_cache)
if not changed:
return column, False
if pa.types.is_large_list(column.type):
result = pa.LargeListArray.from_arrays(offsets, values)
list_type = pa.large_list(pa.field(
column.type.value_field.name,
values.type,
nullable=column.type.value_field.nullable,
metadata=column.type.value_field.metadata,
))
else:
result = pa.ListArray.from_arrays(offsets, values)
list_type = pa.list_(pa.field(
column.type.value_field.name,
values.type,
nullable=column.type.value_field.nullable,
metadata=column.type.value_field.metadata,
))
return pa.Array.from_buffers(
list_type,
len(result),
result.buffers()[:2],
null_count=result.null_count,
children=[values],
), True
if isinstance(data_type, MapType) and pa.types.is_map(column.type):
offsets, start, end = _normalized_offsets(column)
keys = column.keys.slice(start, end - start)
items = column.items.slice(start, end - start)
keys, key_changed = _assemble_variant_column(
keys, data_type.key, schema_cache)
items, item_changed = _assemble_variant_column(
items, data_type.value, schema_cache)
if not key_changed and not item_changed:
return column, False
result = pa.MapArray.from_arrays(offsets, keys, items)
map_type = pa.map_(
pa.field(
column.type.key_field.name,
keys.type,
nullable=False,
metadata=column.type.key_field.metadata,
),
pa.field(
column.type.item_field.name,
items.type,
nullable=column.type.item_field.nullable,
metadata=column.type.item_field.metadata,
),
keys_sorted=getattr(column.type, 'keys_sorted', False),
)
entries = pa.StructArray.from_arrays(
[keys, items], fields=[map_type.key_field, map_type.item_field])
return pa.Array.from_buffers(
map_type,
len(result),
result.buffers()[:2],
null_count=result.null_count,
children=[entries],
), True
return column, False
def _normalized_offsets(column):
offsets_array = getattr(column, 'offsets', None)
if offsets_array is None:
offsets_array = pa.Array.from_buffers(
pa.int32(),
len(column) + 1,
[None, column.buffers()[1]],
offset=column.offset,
)
raw_offsets = offsets_array.to_pylist()
start = raw_offsets[0]
end = raw_offsets[-1]
offsets = [value - start for value in raw_offsets]
for index, is_null in enumerate(column.is_null().to_pylist()):
if is_null:
offsets[index] = None
return pa.array(offsets, type=offsets_array.type), start, end