blob: f76081d5a6376a6fc5af54880d52594e92bcc87c [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.
"""Lance materialization respects known descriptor storage provenance."""
import shutil
import pyarrow as pa
import pytest
from pypaimon.table.row.blob import BlobDescriptor, VideoFrameDescriptor
from pypaimon.tests import multimodal_lance_test as lance_tests
conn = lance_tests.conn
lance = lance_tests.lance
def _descriptor(path, version, offset, length, padding=b""):
encoded = BlobDescriptor(str(path), offset, length).serialize()
if version < 2:
encoded = bytes([version]) + encoded[9:]
return encoded + padding
@pytest.mark.parametrize("version", [0, 1, 2])
@pytest.mark.parametrize("padding", [b"", b"padding"])
@pytest.mark.parametrize("option", ["blob-descriptor-field", "blob.stored-descriptor-fields"])
def test_export_lance_materializes_known_descriptor_wire_versions(conn, tmp_path, version, padding, option):
external = tmp_path / "external.bin"
external.write_bytes(b"0123456789")
table = lance_tests._table(conn, "descriptors", {"id": pa.int32(), "payload": pa.large_binary()}, [
{"id": 1, "payload": _descriptor(external, version, 2, 5, padding)},
{"id": 2, "payload": None},
{"id": 3, "payload": _descriptor(external, version, 7, 0, padding)},
], options={option: "payload"})
expected = [{"payload": b"23456", "id": 1}, {"payload": None, "id": 2}, {"payload": b"", "id": 3}]
assert table.copy({"blob-as-descriptor": "false"}).scan().select(["payload", "id"]).to_list() == expected
path = tmp_path / "descriptors.lance"
table.scan().where("id <= 3").select(["payload", "id"]).export_lance(str(path))
external.unlink()
shutil.rmtree(str(tmp_path / "warehouse"))
assert lance.dataset(str(path)).schema.names == ["payload", "id"]
assert lance_tests._rows(path) == expected
def test_export_lance_materializes_descriptor_field_with_colliding_projection_alias(conn, tmp_path):
external = tmp_path / "external.bin"
external.write_bytes(b"prefix-body-suffix")
table = lance_tests._table(conn, "descriptor_alias", {
"id": pa.int32(), "media": pa.struct([("payload", pa.string())]),
"media_payload": pa.large_binary(),
}, [{"id": 1, "media": {"payload": "ordinary"}, "media_payload": _descriptor(external, 1, 7, 4)}],
options={"blob-descriptor-field": "media_payload"})
columns = ["media.payload", "media_payload", "id"]
expected = [{"media_payload": "ordinary", "media_payload__0": b"body", "id": 1}]
assert table.copy({"blob-as-descriptor": "false"}).scan().select(columns).to_list() == expected
path = tmp_path / "descriptor_alias.lance"
table.scan().select(columns).export_lance(str(path))
external.unlink()
assert lance_tests._rows(path) == expected
@pytest.mark.parametrize("version", [0, 1])
@pytest.mark.parametrize("padding", [b"", b"padding"])
def test_export_lance_keeps_legacy_descriptor_shaped_managed_payloads_raw(conn, tmp_path, version, padding):
raw = _descriptor(tmp_path / "must-not-be-read.bin", version, 2, 5, padding)
table = lance_tests._table(conn, "raw_payloads", {"id": pa.int32(), "payload": pa.large_binary()}, [
{"id": 1, "payload": raw}, {"id": 2, "payload": None}, {"id": 3, "payload": b""},
])
path = tmp_path / "raw_payloads.lance"
table.scan().export_lance(str(path))
shutil.rmtree(str(tmp_path / "warehouse"))
assert lance_tests._rows(path) == [
{"id": 1, "payload": raw}, {"id": 2, "payload": None}, {"id": 3, "payload": b""},
]
@pytest.mark.parametrize("mode", ["create", "overwrite"])
def test_export_lance_unresolvable_legacy_descriptor_does_not_publish(conn, tmp_path, mode):
table = lance_tests._table(conn, "missing_reference", {"id": pa.int32(), "payload": pa.large_binary()}, [
{"id": 1, "payload": _descriptor(tmp_path / "missing.bin", 1, 2, 5)},
], options={"blob-descriptor-field": "payload"})
path = tmp_path / "missing_reference.lance"
if mode == "overwrite":
lance.write_dataset(pa.table({"id": [99]}), str(path))
with pytest.raises((OSError, RuntimeError)):
table.scan().export_lance(str(path), mode=mode)
if mode == "overwrite":
assert lance.dataset(str(path)).to_table().to_pylist() == [{"id": 99}]
else:
with pytest.raises(ValueError):
lance.dataset(str(path))
@pytest.mark.parametrize("mode", ["create", "overwrite"])
def test_export_lance_rejects_video_frame_in_ordinary_descriptor_field(conn, tmp_path, mode):
video = tmp_path / "video.bin"
video.write_bytes(b"encoded-video")
descriptor = VideoFrameDescriptor(str(video), 0, video.stat().st_size, 2)
# Seed the invalid legacy value through Python; Native rejects it before export.
table = lance_tests._table(conn, "frame_reference", {"id": pa.int32(), "payload": pa.large_binary()}, [
{"id": 1, "payload": descriptor},
], options={"blob-descriptor-field": "payload", "write.native.enabled": "false"})
path = tmp_path / "frame_reference.lance"
if mode == "overwrite":
lance.write_dataset(pa.table({"id": [99]}), str(path))
with pytest.raises((ValueError, OSError), match="(?i)video"):
table.scan().export_lance(str(path), mode=mode)
if mode == "overwrite":
assert lance.dataset(str(path)).to_table().to_pylist() == [{"id": 99}]
else:
with pytest.raises(ValueError):
lance.dataset(str(path))