blob: ac19ad9d233139f7ded5370bcb82adb26d501fe6 [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.
################################################################################
"""Utilities for deserializing Paimon BlobDescriptor bytes into FileReference arrays."""
from __future__ import annotations
import struct
import pyarrow as pa
from pypaimon.daft.daft_compat import file_range_position_field, file_range_size_field
from pypaimon.table.row.blob import BlobDescriptor
FILE_PHYSICAL_TYPE = pa.struct(
[
pa.field("url", pa.large_utf8()),
pa.field("io_config", pa.large_binary()),
pa.field(file_range_position_field(), pa.int64()),
pa.field(file_range_size_field(), pa.int64()),
]
)
def _deserialize_one(data: bytes) -> tuple[str, int, int]:
"""Deserialize a single BlobDescriptor -> (url, offset, length)."""
try:
descriptor = BlobDescriptor.deserialize(data)
except (struct.error, UnicodeDecodeError) as e:
raise ValueError("Invalid BlobDescriptor data") from e
return descriptor.uri, descriptor.offset, descriptor.length
def blob_column_to_file_array(column: pa.Array, io_config_bytes: bytes | None = None) -> pa.Array:
"""Convert a large_binary column of serialized BlobDescriptors to a File-compatible struct.
``io_config_bytes`` (serialized IOConfig) is embedded into each File so native File ops carry
credentials; when None the io_config is left null and ops fall back to Daft's global IOConfig.
"""
urls: list[str | None] = []
offsets: list[int | None] = []
lengths: list[int | None] = []
for value in column:
if value is None or not value.is_valid:
urls.append(None)
offsets.append(None)
lengths.append(None)
else:
raw = value.as_py()
uri, off, length = _deserialize_one(raw)
urls.append(uri)
offsets.append(off)
lengths.append(length)
n = len(urls)
if io_config_bytes is None:
io_configs: pa.Array = pa.nulls(n, type=pa.large_binary())
else:
# Only populate io_config for valid rows; keep null rows null.
io_configs = pa.array(
[io_config_bytes if u is not None else None for u in urls],
type=pa.large_binary(),
)
return pa.StructArray.from_arrays(
[
pa.array(urls, type=pa.large_utf8()),
io_configs,
pa.array(offsets, type=pa.int64()),
pa.array(lengths, type=pa.int64()),
],
names=["url", "io_config", file_range_position_field(), file_range_size_field()],
)
def blob_array_column_to_file_array(
column: pa.Array,
io_config_bytes: bytes | None = None,
) -> pa.Array:
"""Convert a list of serialized BlobDescriptors to a list of File structs."""
if not (pa.types.is_list(column.type) or pa.types.is_large_list(column.type)):
raise TypeError(f"Expected a list column, but got {column.type}.")
rows = []
for row in column:
if row is None or not row.is_valid:
rows.append(None)
continue
files = []
for raw in row.as_py():
if raw is None:
files.append(None)
continue
uri, offset, length = _deserialize_one(raw)
files.append({
"url": uri,
"io_config": io_config_bytes,
file_range_position_field(): offset,
file_range_size_field(): length,
})
rows.append(files)
value_field = pa.field(
column.type.value_field.name,
FILE_PHYSICAL_TYPE,
nullable=column.type.value_field.nullable,
metadata=column.type.value_field.metadata,
)
target_type = (
pa.large_list(value_field)
if pa.types.is_large_list(column.type)
else pa.list_(value_field)
)
return pa.array(rows, type=target_type)
def blob_map_column_to_file_array(
column: pa.Array,
io_config_bytes: bytes | None = None,
) -> pa.Array:
"""Convert a map of serialized BlobDescriptors to a map of File structs."""
if not pa.types.is_map(column.type):
raise TypeError(f"Expected a map column, but got {column.type}.")
rows = []
for row in column:
if row is None or not row.is_valid:
rows.append(None)
continue
files = []
for key, raw in row.as_py():
if raw is None:
files.append((key, None))
continue
uri, offset, length = _deserialize_one(raw)
files.append((
key,
{
"url": uri,
"io_config": io_config_bytes,
file_range_position_field(): offset,
file_range_size_field(): length,
},
))
rows.append(files)
item_field = pa.field(
column.type.item_field.name,
FILE_PHYSICAL_TYPE,
nullable=column.type.item_field.nullable,
metadata=column.type.item_field.metadata,
)
target_type = pa.map_(
column.type.key_field,
item_field,
keys_sorted=column.type.keys_sorted,
)
return pa.array(rows, type=target_type)