blob: 59c2a37b106f4d79edb4dedf67b076fa74771798 [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 shutil
import tempfile
import unittest
import pyarrow as pa
import pytest
pypaimon = pytest.importorskip("pypaimon")
daft = pytest.importorskip("daft")
from daft import col
from pypaimon import CatalogFactory, Schema
from pypaimon.daft import open_blob, read_blob, read_paimon
from pypaimon.daft.daft_compat import has_file_range_reads
@unittest.skipUnless(has_file_range_reads(), "installed Daft lacks File range reads")
class DaftReadBlobTest(unittest.TestCase):
"""read_blob reads a blob File column to bytes via pypaimon's FileIO and must
return exactly the same bytes as the native per-row File.open() path."""
N = 40
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.catalog_options = {"warehouse": os.path.join(self.tempdir, "wh")}
self.catalog = CatalogFactory.create(self.catalog_options)
self.catalog.create_database("default", True)
self.table = "default.blob_read_test"
pa_schema = pa.schema([("id", pa.int32()), ("content", pa.large_binary())])
self.catalog.create_table(self.table, Schema.from_pyarrow_schema(
pa_schema, options={"row-tracking.enabled": "true", "data-evolution.enabled": "true"}), False)
self.payloads = [bytes([i & 0xFF]) + os.urandom(1023) for i in range(self.N)]
t = self.catalog.get_table(self.table)
w = t.new_batch_write_builder().new_write()
w.write_arrow(pa.Table.from_pydict(
{"id": list(range(self.N)), "content": self.payloads}, schema=pa_schema))
t.new_batch_write_builder().new_commit().commit(w.prepare_commit())
w.close()
def tearDown(self):
shutil.rmtree(self.tempdir, ignore_errors=True)
def test_read_blob_matches_file_open(self):
df = read_paimon(self.table, self.catalog_options)
# content is exposed as a lazy File (default read path unchanged)
self.assertEqual(str(df.schema()["content"].dtype), "File[Unknown]")
# new path: read_blob -> bytes
out = df.select(col("id"), read_blob(col("content"), self.catalog_options, self.table)
.alias("img")).collect().to_pydict()
got = dict(zip(out["id"], out["img"]))
for i in range(self.N):
self.assertEqual(got[i], self.payloads[i])
# original path still works and yields identical bytes
for r in read_paimon(self.table, self.catalog_options).select(col("id"), col("content")).to_pylist():
with r["content"].open() as h:
self.assertEqual(h.read(), self.payloads[r["id"]])
def test_open_blob_full_read(self):
rows = read_paimon(self.table, self.catalog_options).select(
col("id"), col("content")).to_pylist()
for r in rows:
with open_blob(r["content"], self.catalog_options, self.table) as f:
self.assertEqual(f.read(), self.payloads[r["id"]])
def test_open_blob_partial_read(self):
rows = read_paimon(self.table, self.catalog_options).select(
col("id"), col("content")).to_pylist()
for r in rows:
with open_blob(r["content"], self.catalog_options, self.table) as f:
header = f.read(16)
self.assertEqual(header, self.payloads[r["id"]][:16])
def test_open_blob_seek(self):
r = read_paimon(self.table, self.catalog_options).select(
col("id"), col("content")).to_pylist()[0]
with open_blob(r["content"], self.catalog_options, self.table) as f:
f.seek(100)
self.assertEqual(f.tell(), 100)
chunk = f.read(50)
self.assertEqual(chunk, self.payloads[r["id"]][100:150])
def test_no_file_size_stat(self):
"""File.size() does a network stat; read_blob and open_blob must resolve
ranges from embedded metadata (file._inner) and never call it."""
rows = read_paimon(self.table, self.catalog_options).select(
col("id"), col("content")).to_pylist()
file_cls = type(rows[0]["content"])
calls = {"n": 0}
orig = file_cls.size
def boom(self, *a, **k):
calls["n"] += 1
raise AssertionError("File.size() should not be called")
file_cls.size = boom
try:
with open_blob(rows[0]["content"], self.catalog_options, self.table) as f:
self.assertEqual(f.read(), self.payloads[rows[0]["id"]])
out = read_paimon(self.table, self.catalog_options).select(
col("id"), read_blob(col("content"), self.catalog_options, self.table).alias("img")
).collect().to_pydict()
got = dict(zip(out["id"], out["img"]))
for i in range(self.N):
self.assertEqual(got[i], self.payloads[i])
finally:
file_cls.size = orig
self.assertEqual(calls["n"], 0)
if __name__ == "__main__":
unittest.main()