This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 4f289264bf [python] Support constructing BlobView and BlobViewStruct 
by field name (#10608)
4f289264bf is described below

commit 4f289264bf32393d0b09c5b54e7cc9a059dc5ff6
Author: zhuanshenbsj1 <[email protected]>
AuthorDate: Sun Oct 18 16:51:33 2026 +0800

    [python] Support constructing BlobView and BlobViewStruct by field name 
(#10608)
---
 paimon-python/pypaimon/table/row/blob.py        | 94 +++++++++++++++++++++++--
 paimon-python/pypaimon/tests/blob_table_test.py | 38 +++-------
 paimon-python/pypaimon/tests/blob_test.py       | 87 +++++++++++++++++++++++
 3 files changed, 185 insertions(+), 34 deletions(-)

diff --git a/paimon-python/pypaimon/table/row/blob.py 
b/paimon-python/pypaimon/table/row/blob.py
index 317763ab12..769fdfb717 100644
--- a/paimon-python/pypaimon/table/row/blob.py
+++ b/paimon-python/pypaimon/table/row/blob.py
@@ -405,13 +405,75 @@ class BlobViewStruct:
     MAGIC = 0x424C4F4256494557  # "BLOBVIEW"
 
     def __init__(self, identifier: Union[Identifier, str], field_id: int, 
row_id: int):
+        self._identifier = self._coerce_identifier(identifier)
+        self._field_id = field_id
+        self._row_id = row_id
+
+    @staticmethod
+    def _coerce_identifier(identifier: Union[Identifier, str]) -> Identifier:
         if isinstance(identifier, str):
-            identifier = Identifier.from_string(identifier)
+            return Identifier.from_string(identifier)
         if not isinstance(identifier, Identifier):
             raise TypeError("BlobViewStruct identifier must be Identifier or 
str.")
-        self._identifier = identifier
-        self._field_id = field_id
-        self._row_id = row_id
+        return identifier
+
+    @staticmethod
+    def _extract_fields(upstream):
+        if hasattr(upstream, 'fields') and upstream.fields is not None:
+            return upstream.fields
+        if hasattr(upstream, 'table_schema') and 
getattr(upstream.table_schema, 'fields', None) is not None:
+            return upstream.table_schema.fields
+        if isinstance(upstream, (list, tuple)):
+            return upstream
+        raise TypeError("Expected Table, TableSchema, RowType, or sequence of 
DataField.")
+
+    @classmethod
+    def resolve_blob_field_id(
+            cls,
+            identifier: Union[Identifier, str],
+            upstream,
+            field_name: str,
+    ) -> int:
+        from pypaimon.schema.data_types import is_blob_type
+
+        identifier = cls._coerce_identifier(identifier)
+        if not isinstance(field_name, str):
+            raise TypeError("BlobViewStruct field_name must be a str.")
+        fields = cls._extract_fields(upstream)
+        matched_field = None
+        for field in fields:
+            if field.name == field_name:
+                matched_field = field
+                break
+        if matched_field is None:
+            raise ValueError(
+                f"Cannot find blob field {field_name} in upstream table 
{identifier.get_full_name()}."
+            )
+        if not is_blob_type(matched_field.type):
+            raise ValueError(
+                f"Field {field_name} in upstream table 
{identifier.get_full_name()} is not a BLOB field."
+            )
+        return matched_field.id
+
+    @classmethod
+    def of(cls, *args) -> 'BlobViewStruct':
+        if len(args) == 3:
+            upstream_table, field_name, row_id = args
+            if not hasattr(upstream_table, 'identifier'):
+                raise TypeError(
+                    "3-argument BlobViewStruct.of expects an upstream Table 
with an identifier."
+                )
+            identifier = upstream_table.identifier
+            field_id = cls.resolve_blob_field_id(identifier, upstream_table, 
field_name)
+            return cls(identifier, field_id, row_id)
+        if len(args) == 4:
+            identifier, upstream, field_name, row_id = args
+            field_id = cls.resolve_blob_field_id(identifier, upstream, 
field_name)
+            return cls(identifier, field_id, row_id)
+        raise TypeError(
+            "BlobViewStruct.of expects (upstream_table, field_name, row_id) or 
"
+            "(identifier, upstream_schema, field_name, row_id)."
+        )
 
     @property
     def identifier(self) -> Identifier:
@@ -672,8 +734,28 @@ class Blob(ABC):
             descriptor, file_io=file_io, uri_reader_factory=uri_reader_factory)
 
     @staticmethod
-    def from_view(view_struct: BlobViewStruct) -> 'BlobView':
-        return BlobView(view_struct)
+    def from_view(*args, view_struct: Optional[BlobViewStruct] = None) -> 
'BlobView':
+        # Case 1: Keyword argument form (backward compatible)
+        if view_struct is not None:
+            if len(args) > 0:
+                raise ValueError("Cannot provide both view_struct keyword 
argument and positional arguments")
+            return BlobView(view_struct)
+        # Case 2: Positional argument forms
+        if len(args) == 1 and isinstance(args[0], BlobViewStruct):
+            return BlobView(args[0])
+        if len(args) == 3:
+            target, field, row_id = args
+            if isinstance(field, int) and not isinstance(field, bool):
+                return BlobView(BlobViewStruct(target, field, row_id))
+            if isinstance(field, str):
+                return BlobView(BlobViewStruct.of(target, field, row_id))
+        if len(args) == 4:
+            return BlobView(BlobViewStruct.of(*args))
+        raise TypeError(
+            "Blob.from_view expects (BlobViewStruct), (identifier, field_id, 
row_id), "
+            "(upstream_table, field_name, row_id), or "
+            "(identifier, upstream_schema, field_name, row_id)."
+        )
 
     @staticmethod
     def from_bytes(
diff --git a/paimon-python/pypaimon/tests/blob_table_test.py 
b/paimon-python/pypaimon/tests/blob_table_test.py
index bf6612bf15..458792633f 100755
--- a/paimon-python/pypaimon/tests/blob_table_test.py
+++ b/paimon-python/pypaimon/tests/blob_table_test.py
@@ -2722,12 +2722,9 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         write_builder.new_commit().commit(commit_messages)
         writer.close()
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_source', picture_field_id, 
0).serialize(),
-            BlobViewStruct('test_db.blob_view_source', picture_field_id, 
1).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 0).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 1).serialize(),
         ]
 
         target_schema = pa.schema([
@@ -2842,12 +2839,9 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         }
         self.assertGreater(len(source_blob_paths), 0)
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_guard_source', picture_field_id, 
0).serialize(),
-            BlobViewStruct('test_db.blob_view_guard_source', picture_field_id, 
1).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 0).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 1).serialize(),
         ]
 
         target_schema = pa.schema([
@@ -2925,12 +2919,9 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         write_builder.new_commit().commit(commit_messages)
         writer.close()
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_resolve_source', 
picture_field_id, 0).serialize(),
-            BlobViewStruct('test_db.blob_view_resolve_source', 
picture_field_id, 1).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 0).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 1).serialize(),
         ]
 
         target_schema = pa.schema([
@@ -3009,12 +3000,9 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         write_builder.new_commit().commit(commit_messages)
         writer.close()
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_null_source', picture_field_id, 
0).serialize(),
-            BlobViewStruct('test_db.blob_view_null_source', picture_field_id, 
1).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 0).serialize(),
+            BlobViewStruct.of(source_table, 'picture', 1).serialize(),
         ]
 
         target_schema = pa.schema([
@@ -3146,11 +3134,8 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         write_builder.new_commit().commit(commit_messages)
         writer.close()
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_limit_source', picture_field_id, 
i).serialize()
+            BlobViewStruct.of(source_table, 'picture', i).serialize()
             for i in range(num_rows)
         ]
 
@@ -3230,11 +3215,8 @@ class DedicatedFormatWriterTest(unittest.TestCase):
         write_builder.new_commit().commit(commit_messages)
         writer.close()
 
-        picture_field_id = next(
-            field.id for field in source_table.table_schema.fields if 
field.name == 'picture'
-        )
         view_values = [
-            BlobViewStruct('test_db.blob_view_prescan_count_source', 
picture_field_id, i).serialize()
+            BlobViewStruct.of(source_table, 'picture', i).serialize()
             for i in range(num_rows)
         ]
 
diff --git a/paimon-python/pypaimon/tests/blob_test.py 
b/paimon-python/pypaimon/tests/blob_test.py
index 0043fd9833..3f4f2ebd44 100644
--- a/paimon-python/pypaimon/tests/blob_test.py
+++ b/paimon-python/pypaimon/tests/blob_test.py
@@ -380,6 +380,55 @@ class BlobTest(unittest.TestCase):
         self.assertFalse(blob.is_resolved())
         self.assertEqual(blob.view_struct, view_struct)
 
+    def test_blob_view_struct_construct_by_field_name(self):
+        from types import SimpleNamespace
+        from pypaimon.common.identifier import Identifier
+        from pypaimon.schema.data_types import AtomicType, DataField, RowType
+
+        fields = [
+            DataField(0, "id", AtomicType("INT")),
+            DataField(7, "picture", AtomicType("BLOB")),
+        ]
+        row_type = RowType(True, fields)
+        fake_table = SimpleNamespace(
+            identifier=Identifier.from_string("test_db.source_table"),
+            fields=fields,
+        )
+
+        expected = BlobViewStruct("test_db.source_table", 7, 42)
+        self.assertEqual(
+            BlobViewStruct.of("test_db.source_table", row_type, "picture", 42),
+            expected,
+        )
+        self.assertEqual(
+            BlobViewStruct.of(fake_table, "picture", 42),
+            expected,
+        )
+        self.assertEqual(
+            Blob.from_view("test_db.source_table", 7, 42),
+            Blob.from_view(expected),
+        )
+        self.assertEqual(
+            Blob.from_view("test_db.source_table", row_type, "picture", 42),
+            Blob.from_view(expected),
+        )
+        self.assertEqual(
+            Blob.from_view(fake_table, "picture", 42),
+            Blob.from_view(expected),
+        )
+
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Cannot find blob field missing in upstream table 
test_db\.source_table\.",
+        ):
+            BlobViewStruct.of(fake_table, "missing", 42)
+
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Field id in upstream table test_db\.source_table is not a BLOB 
field\.",
+        ):
+            BlobViewStruct.of(fake_table, "id", 42)
+
     def test_blob_fallback_batch_reader_respects_batch_size(self):
         created_readers = []
 
@@ -6757,5 +6806,43 @@ class OffsetInputStreamTest(unittest.TestCase):
             os.remove(tmp_path)
 
 
+class BlobFromViewBackwardCompatibilityTest(unittest.TestCase):
+    """Test backward compatibility of Blob.from_view keyword argument."""
+
+    def test_from_view_keyword_argument_backward_compatible(self):
+        """Test that existing keyword-argument calls still work."""
+        from pypaimon.common.identifier import Identifier
+        from pypaimon.table.row.blob import Blob, BlobViewStruct
+
+        view_struct = 
BlobViewStruct(Identifier.from_string("default.test_compat"), 1, 42)
+        blob_view = Blob.from_view(view_struct=view_struct)
+
+        self.assertEqual(blob_view.view_struct.identifier.get_full_name(), 
"default.test_compat")
+        self.assertEqual(blob_view.view_struct.field_id, 1)
+        self.assertEqual(blob_view.view_struct.row_id, 42)
+
+    def test_from_view_positional_argument_still_works(self):
+        """Test that positional argument form still works."""
+        from pypaimon.common.identifier import Identifier
+        from pypaimon.table.row.blob import Blob, BlobViewStruct
+
+        view_struct = 
BlobViewStruct(Identifier.from_string("default.test_pos"), 2, 100)
+        blob_view = Blob.from_view(view_struct)
+
+        self.assertEqual(blob_view.view_struct.identifier.get_full_name(), 
"default.test_pos")
+        self.assertEqual(blob_view.view_struct.field_id, 2)
+        self.assertEqual(blob_view.view_struct.row_id, 100)
+
+    def test_from_view_cannot_mix_keyword_and_positional(self):
+        """Test that mixing keyword and positional is rejected."""
+        from pypaimon.common.identifier import Identifier
+        from pypaimon.table.row.blob import Blob, BlobViewStruct
+
+        view_struct = 
BlobViewStruct(Identifier.from_string("default.test_mix"), 3, 200)
+        with self.assertRaises(ValueError) as cm:
+            Blob.from_view(view_struct, view_struct=view_struct)
+        self.assertIn("Cannot provide both", str(cm.exception))
+
+
 if __name__ == '__main__':
     unittest.main()

Reply via email to