@@ -5811,6 +5811,219 @@ def test_blob_view_as_descriptor_projection_with_predicate_extra_field(self):
58115811 )
58125812 self .assertFalse (BlobViewStruct .is_blob_view_struct (picture_bytes ))
58135813
5814+ def test_primary_key_blob_descriptor_field_write_and_upsert (self ):
5815+ from pypaimon import Schema
5816+ from pypaimon .table .row .blob import Blob , BlobDescriptor
5817+ from pypaimon .table .row .generic_row import GenericRow
5818+
5819+ blob_file = os .path .join (self .temp_dir , 'pk_descriptor_payloads.bin' )
5820+ with open (blob_file , 'wb' ) as f :
5821+ f .write (b'payload-v1payload-v2payload-row-3' )
5822+
5823+ desc_v1 = BlobDescriptor (blob_file , 0 , 10 )
5824+ desc_v2 = BlobDescriptor (blob_file , 10 , 10 )
5825+ desc_row3 = BlobDescriptor (blob_file , 20 , 13 )
5826+
5827+ pa_schema = pa .schema ([
5828+ pa .field ('id' , pa .int32 (), nullable = False ),
5829+ pa .field ('name' , pa .string ()),
5830+ pa .field ('picture' , pa .large_binary ()),
5831+ ])
5832+ schema = Schema .from_pyarrow_schema (
5833+ pa_schema ,
5834+ primary_keys = ['id' ],
5835+ options = {
5836+ 'bucket' : '1' ,
5837+ 'blob-descriptor-field' : 'picture' ,
5838+ },
5839+ )
5840+ self .catalog .create_table ('test_db.pk_blob_descriptor_table' , schema , False )
5841+ table = self .catalog .get_table ('test_db.pk_blob_descriptor_table' )
5842+
5843+ # 1. Invalid descriptor payload is rejected by the PK writer
5844+ write_builder = table .new_batch_write_builder ()
5845+ bad_writer = write_builder .new_write ()
5846+ with self .assertRaisesRegex (
5847+ ValueError ,
5848+ "blob-descriptor-field requires blob field value to be a serialized BlobDescriptor." ,
5849+ ):
5850+ bad_writer .write_arrow (pa .Table .from_pydict ({
5851+ 'id' : [1 ],
5852+ 'name' : ['bad' ],
5853+ 'picture' : [b'not-a-descriptor' ],
5854+ }, schema = pa_schema ))
5855+ bad_writer .close ()
5856+
5857+ # 2. Initial commit via write_arrow + write_row
5858+ writer1 = write_builder .new_write ()
5859+ writer1 .write_arrow (pa .Table .from_pydict ({
5860+ 'id' : [1 , 2 ],
5861+ 'name' : ['alice-v1' , 'bob' ],
5862+ 'picture' : [desc_v1 .serialize (), None ],
5863+ }, schema = pa_schema ))
5864+ writer1 .write_row (GenericRow (
5865+ [
5866+ 3 ,
5867+ 'charlie' ,
5868+ Blob .from_descriptor_bytes (
5869+ desc_row3 .serialize (), file_io = table .file_io
5870+ ),
5871+ ],
5872+ table .fields ,
5873+ ))
5874+ write_builder .new_commit ().commit (writer1 .prepare_commit ())
5875+ writer1 .close ()
5876+
5877+ # 3. Second commit upserting id=1 with desc_v2
5878+ writer2 = write_builder .new_write ()
5879+ writer2 .write_arrow (pa .Table .from_pydict ({
5880+ 'id' : [1 ],
5881+ 'name' : ['alice-v2' ],
5882+ 'picture' : [desc_v2 .serialize ()],
5883+ }, schema = pa_schema ))
5884+ write_builder .new_commit ().commit (writer2 .prepare_commit ())
5885+ writer2 .close ()
5886+
5887+ read_builder = table .new_read_builder ()
5888+ result = read_builder .new_read ().to_arrow (
5889+ read_builder .new_scan ().plan ().splits ()
5890+ ).sort_by ('id' )
5891+ self .assertEqual (result .column ('id' ).to_pylist (), [1 , 2 , 3 ])
5892+ self .assertEqual (
5893+ result .column ('name' ).to_pylist (),
5894+ ['alice-v2' , 'bob' , 'charlie' ],
5895+ )
5896+ self .assertEqual (
5897+ result .column ('picture' ).to_pylist (),
5898+ [b'payload-v2' , None , b'payload-row-3' ],
5899+ )
5900+
5901+ def test_primary_key_blob_view_field_write_and_upsert (self ):
5902+ from pypaimon import Schema
5903+ from pypaimon .table .row .blob import Blob , BlobViewStruct
5904+ from pypaimon .table .row .generic_row import GenericRow
5905+
5906+ source_schema = pa .schema ([
5907+ ('id' , pa .int32 ()),
5908+ ('picture' , pa .large_binary ()),
5909+ ])
5910+ source = Schema .from_pyarrow_schema (
5911+ source_schema ,
5912+ options = {
5913+ 'row-tracking.enabled' : 'true' ,
5914+ 'data-evolution.enabled' : 'true' ,
5915+ },
5916+ )
5917+ self .catalog .create_table ('test_db.pk_blob_view_source' , source , False )
5918+ source_table = self .catalog .get_table ('test_db.pk_blob_view_source' )
5919+
5920+ src_wb = source_table .new_batch_write_builder ()
5921+ src_writer = src_wb .new_write ()
5922+ src_writer .write_arrow (pa .Table .from_pydict ({
5923+ 'id' : [10 , 20 , 30 ],
5924+ 'picture' : [b'upstream-0' , b'upstream-1' , b'upstream-2' ],
5925+ }, schema = source_schema ))
5926+ src_wb .new_commit ().commit (src_writer .prepare_commit ())
5927+ src_writer .close ()
5928+
5929+ target_schema = pa .schema ([
5930+ pa .field ('id' , pa .int32 (), nullable = False ),
5931+ pa .field ('label' , pa .string ()),
5932+ pa .field ('picture' , pa .large_binary ()),
5933+ ])
5934+ target = Schema .from_pyarrow_schema (
5935+ target_schema ,
5936+ primary_keys = ['id' ],
5937+ options = {
5938+ 'bucket' : '1' ,
5939+ 'blob-view-field' : 'picture' ,
5940+ },
5941+ )
5942+ self .catalog .create_table ('test_db.pk_blob_view_target' , target , False )
5943+ target_table = self .catalog .get_table ('test_db.pk_blob_view_target' )
5944+
5945+ view_0 = BlobViewStruct .of ('test_db.pk_blob_view_source' , source_table , 'picture' , 0 )
5946+ view_1 = BlobViewStruct .of ('test_db.pk_blob_view_source' , source_table , 'picture' , 1 )
5947+ view_2 = BlobViewStruct .of ('test_db.pk_blob_view_source' , source_table , 'picture' , 2 )
5948+
5949+ # 1. Invalid BlobViewStruct bytes are rejected by the PK writer
5950+ target_wb = target_table .new_batch_write_builder ()
5951+ bad_writer = target_wb .new_write ()
5952+ with self .assertRaisesRegex (
5953+ ValueError ,
5954+ "blob-view-field requires blob field value to be a serialized BlobViewStruct." ,
5955+ ):
5956+ bad_writer .write_arrow (pa .Table .from_pydict ({
5957+ 'id' : [1 ],
5958+ 'label' : ['bad' ],
5959+ 'picture' : [b'invalid-view-bytes' ],
5960+ }, schema = target_schema ))
5961+ bad_writer .close ()
5962+
5963+ # 2. First commit via write_arrow + write_row
5964+ writer1 = target_wb .new_write ()
5965+ writer1 .write_arrow (pa .Table .from_pydict ({
5966+ 'id' : [1 , 2 ],
5967+ 'label' : ['row1-v1' , 'row2' ],
5968+ 'picture' : [view_0 .serialize (), None ],
5969+ }, schema = target_schema ))
5970+ writer1 .write_row (GenericRow (
5971+ [3 , 'row3' , Blob .from_view (view_2 )],
5972+ target_table .fields ,
5973+ ))
5974+ target_wb .new_commit ().commit (writer1 .prepare_commit ())
5975+ writer1 .close ()
5976+
5977+ # 3. Second commit upserting id=1 to point to upstream row 1
5978+ writer2 = target_wb .new_write ()
5979+ writer2 .write_arrow (pa .Table .from_pydict ({
5980+ 'id' : [1 ],
5981+ 'label' : ['row1-v2' ],
5982+ 'picture' : [view_1 .serialize ()],
5983+ }, schema = target_schema ))
5984+ target_wb .new_commit ().commit (writer2 .prepare_commit ())
5985+ writer2 .close ()
5986+
5987+ read_builder = target_table .new_read_builder ()
5988+ result = read_builder .new_read ().to_arrow (
5989+ read_builder .new_scan ().plan ().splits ()
5990+ ).sort_by ('id' )
5991+ self .assertEqual (result .column ('id' ).to_pylist (), [1 , 2 , 3 ])
5992+ self .assertEqual (
5993+ result .column ('label' ).to_pylist (),
5994+ ['row1-v2' , 'row2' , 'row3' ],
5995+ )
5996+ self .assertEqual (
5997+ result .column ('picture' ).to_pylist (),
5998+ [b'upstream-1' , None , b'upstream-2' ],
5999+ )
6000+
6001+ def test_primary_key_managed_blob_requires_native_writer (self ):
6002+ from pypaimon import Schema
6003+
6004+ pa_schema = pa .schema ([
6005+ pa .field ('id' , pa .int32 (), nullable = False ),
6006+ pa .field ('picture' , pa .large_binary ()),
6007+ ])
6008+ schema = Schema .from_pyarrow_schema (
6009+ pa_schema ,
6010+ primary_keys = ['id' ],
6011+ options = {'bucket' : '1' },
6012+ )
6013+ self .catalog .create_table ('test_db.pk_managed_blob_table' , schema , False )
6014+ table = self .catalog .get_table ('test_db.pk_managed_blob_table' )
6015+
6016+ writer = table .new_batch_write_builder ().new_write ()
6017+ with self .assertRaisesRegex (
6018+ NotImplementedError ,
6019+ "Primary-key Blob writes require the native writer" ,
6020+ ):
6021+ writer .write_arrow (pa .Table .from_pydict ({
6022+ 'id' : [1 ],
6023+ 'picture' : [b'raw-blob-bytes' ],
6024+ }, schema = pa_schema ))
6025+ writer .close ()
6026+
58146027
58156028class GetBlobTest (unittest .TestCase ):
58166029
0 commit comments