Skip to content

Commit c713303

Browse files
committed
[python] Normalize inline BLOB rows before postpone bucket planning
1 parent e4be772 commit c713303

4 files changed

Lines changed: 194 additions & 9 deletions

File tree

‎paimon-python/pypaimon/execution/python/write/file_store_write.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from pypaimon.schema.data_types import is_blob_file_field, is_blob_type
2626
from pypaimon.write.commit_message import CommitMessage
2727
from pypaimon.write.row_utils import (
28-
inline_blob_value,
28+
normalize_inline_blob_row,
2929
require_columns,
3030
row_values_to_arrow_table,
3131
)
@@ -135,14 +135,14 @@ def write_row(
135135
descriptor_fields = CoreOptions.blob_descriptor_fields(self.options)
136136
view_fields = CoreOptions.blob_view_fields(self.options)
137137
inline_columns = [name for name in column_names if name in descriptor_fields or name in view_fields]
138-
if inline_columns:
139-
values_by_name = dict(values_by_name)
140-
for name in inline_columns:
141-
values_by_name[name] = inline_blob_value(
142-
values_by_name[name], name in descriptor_fields, name in view_fields)
143-
from pypaimon.table.row.generic_row import GenericRow
144-
row = GenericRow([values_by_name[name] for name in column_names],
145-
[self.table.field_dict[name] for name in column_names], row.get_row_kind())
138+
row, values_by_name = normalize_inline_blob_row(
139+
row,
140+
values_by_name,
141+
self.table.field_dict,
142+
column_names,
143+
descriptor_fields,
144+
view_fields,
145+
)
146146
if not self.table.is_primary_key_table:
147147
if writer is None:
148148
ordinary_columns = [

‎paimon-python/pypaimon/execution/python/write/postpone_batch_table_write.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
import pyarrow as pa
2020

21+
from pypaimon.common.options.core_options import CoreOptions
2122
from pypaimon.write.commit_message import CommitMessage
2223
from pypaimon.execution.python.write.file_store_write import PostponeFixedBucketFileStoreWrite
2324
from pypaimon.execution.python.write.postpone_bucket import PythonPostponeBucketPlanner as PostponeBucketPlanner
2425
from pypaimon.execution.python.write.table_write import PythonTableWrite
2526
from pypaimon.execution.python.write.row_key_extractor import PostponeFixedBucketRowKeyExtractor
2627
from pypaimon.write.row_utils import (
28+
normalize_inline_blob_row,
2729
require_columns,
2830
row_to_named_values,
2931
row_values_to_arrow_table,
@@ -129,6 +131,14 @@ def write_row(self, row):
129131
if self._bucket_plan.contains(partition):
130132
return super().write_row(row)
131133

134+
row, values_by_name = normalize_inline_blob_row(
135+
row,
136+
values_by_name,
137+
self.table.field_dict,
138+
column_names,
139+
CoreOptions.blob_descriptor_fields(self.table.options),
140+
CoreOptions.blob_view_fields(self.table.options),
141+
)
132142
arrow_row = row_values_to_arrow_table(
133143
values_by_name, self.table.table_schema.fields, column_names)
134144
size = self._planner.input_partition_stats(arrow_row)[partition][1]

‎paimon-python/pypaimon/tests/blob_table_test.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6027,6 +6027,149 @@ def test_primary_key_managed_blob_requires_native_writer(self):
60276027
}, schema=pa_schema))
60286028
writer.close()
60296029

6030+
def test_postpone_fixed_bucket_pk_inline_blob_write_row_new_partition(self):
6031+
from pypaimon import Schema
6032+
from pypaimon.table.row.blob import Blob, BlobDescriptor, BlobViewStruct
6033+
from pypaimon.table.row.generic_row import GenericRow
6034+
6035+
pk_pa_schema = pa.schema([
6036+
pa.field('pt', pa.string(), nullable=False),
6037+
pa.field('id', pa.int32(), nullable=False),
6038+
pa.field('picture', pa.large_binary()),
6039+
])
6040+
with self.subTest(field='blob-descriptor-field'):
6041+
blob_file = os.path.join(self.temp_dir, 'postpone_pk_descriptor.bin')
6042+
with open(blob_file, 'wb') as f:
6043+
f.write(b'postpone-desc-0postpone-desc-1')
6044+
desc_0 = BlobDescriptor(blob_file, 0, 15)
6045+
desc_1 = BlobDescriptor(blob_file, 15, 15)
6046+
6047+
desc_schema = Schema.from_pyarrow_schema(
6048+
pk_pa_schema,
6049+
primary_keys=['pt', 'id'],
6050+
partition_keys=['pt'],
6051+
options={
6052+
'bucket': '-2',
6053+
'postpone.batch-write-fixed-bucket': 'true',
6054+
'blob-descriptor-field': 'picture',
6055+
'write.native.enabled': 'false',
6056+
},
6057+
)
6058+
self.catalog.create_table('test_db.pk_postpone_blob_descriptor', desc_schema, False)
6059+
desc_table = self.catalog.get_table('test_db.pk_postpone_blob_descriptor')
6060+
6061+
desc_wb = desc_table.new_postpone_fixed_bucket_write_builder()
6062+
desc_writer = desc_wb.new_write()
6063+
try:
6064+
desc_writer.write_row(GenericRow(
6065+
[
6066+
'p0',
6067+
1,
6068+
Blob.from_descriptor_bytes(
6069+
desc_0.serialize(), file_io=desc_table.file_io
6070+
),
6071+
],
6072+
desc_table.fields,
6073+
))
6074+
desc_writer.write_row(GenericRow(
6075+
[
6076+
'p1',
6077+
2,
6078+
Blob.from_descriptor_bytes(
6079+
desc_1.serialize(), file_io=desc_table.file_io
6080+
),
6081+
],
6082+
desc_table.fields,
6083+
))
6084+
messages = desc_writer.prepare_commit()
6085+
self.assertEqual({message.partition for message in messages}, {('p0',), ('p1',)})
6086+
for message in messages:
6087+
self.assertEqual(message.total_buckets, 1)
6088+
desc_wb.new_commit().commit(messages)
6089+
finally:
6090+
desc_writer.close()
6091+
6092+
desc_rb = desc_table.new_read_builder()
6093+
desc_result = desc_rb.new_read().to_arrow(
6094+
desc_rb.new_scan().plan().splits()
6095+
).sort_by('id')
6096+
self.assertEqual(desc_result.column('pt').to_pylist(), ['p0', 'p1'])
6097+
self.assertEqual(desc_result.column('id').to_pylist(), [1, 2])
6098+
self.assertEqual(
6099+
desc_result.column('picture').to_pylist(),
6100+
[b'postpone-desc-0', b'postpone-desc-1'],
6101+
)
6102+
6103+
with self.subTest(field='blob-view-field'):
6104+
source_schema = pa.schema([
6105+
('id', pa.int32()),
6106+
('picture', pa.large_binary()),
6107+
])
6108+
source = Schema.from_pyarrow_schema(
6109+
source_schema,
6110+
options={
6111+
'row-tracking.enabled': 'true',
6112+
'data-evolution.enabled': 'true',
6113+
},
6114+
)
6115+
self.catalog.create_table('test_db.pk_postpone_view_source', source, False)
6116+
source_table = self.catalog.get_table('test_db.pk_postpone_view_source')
6117+
src_wb = source_table.new_batch_write_builder()
6118+
src_writer = src_wb.new_write()
6119+
src_writer.write_arrow(pa.Table.from_pydict({
6120+
'id': [10, 20],
6121+
'picture': [b'postpone-view-0', b'postpone-view-1'],
6122+
}, schema=source_schema))
6123+
src_wb.new_commit().commit(src_writer.prepare_commit())
6124+
src_writer.close()
6125+
6126+
view_0 = BlobViewStruct.of('test_db.pk_postpone_view_source', source_table, 'picture', 0)
6127+
view_1 = BlobViewStruct.of('test_db.pk_postpone_view_source', source_table, 'picture', 1)
6128+
6129+
view_schema = Schema.from_pyarrow_schema(
6130+
pk_pa_schema,
6131+
primary_keys=['pt', 'id'],
6132+
partition_keys=['pt'],
6133+
options={
6134+
'bucket': '-2',
6135+
'postpone.batch-write-fixed-bucket': 'true',
6136+
'blob-view-field': 'picture',
6137+
'write.native.enabled': 'false',
6138+
},
6139+
)
6140+
self.catalog.create_table('test_db.pk_postpone_blob_view', view_schema, False)
6141+
view_table = self.catalog.get_table('test_db.pk_postpone_blob_view')
6142+
6143+
view_wb = view_table.new_postpone_fixed_bucket_write_builder()
6144+
view_writer = view_wb.new_write()
6145+
try:
6146+
view_writer.write_row(GenericRow(
6147+
['p0', 1, Blob.from_view(view_0)],
6148+
view_table.fields,
6149+
))
6150+
view_writer.write_row(GenericRow(
6151+
['p1', 2, Blob.from_view(view_1)],
6152+
view_table.fields,
6153+
))
6154+
messages = view_writer.prepare_commit()
6155+
self.assertEqual({message.partition for message in messages}, {('p0',), ('p1',)})
6156+
for message in messages:
6157+
self.assertEqual(message.total_buckets, 1)
6158+
view_wb.new_commit().commit(messages)
6159+
finally:
6160+
view_writer.close()
6161+
6162+
view_rb = view_table.new_read_builder()
6163+
view_result = view_rb.new_read().to_arrow(
6164+
view_rb.new_scan().plan().splits()
6165+
).sort_by('id')
6166+
self.assertEqual(view_result.column('pt').to_pylist(), ['p0', 'p1'])
6167+
self.assertEqual(view_result.column('id').to_pylist(), [1, 2])
6168+
self.assertEqual(
6169+
view_result.column('picture').to_pylist(),
6170+
[b'postpone-view-0', b'postpone-view-1'],
6171+
)
6172+
60306173

60316174
class GetBlobTest(unittest.TestCase):
60326175

‎paimon-python/pypaimon/write/row_utils.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,38 @@ def inline_blob_value(value: Any, descriptor: bool, view: bool) -> Any:
9090
return value
9191

9292

93+
def normalize_inline_blob_row(
94+
row: InternalRow,
95+
values_by_name: Dict[str, Any],
96+
field_dict: Dict[str, DataField],
97+
column_names: List[str],
98+
descriptor_fields: Iterable[str],
99+
view_fields: Iterable[str]):
100+
"""Normalize inline BLOB reference fields in a row and value map to bytes."""
101+
descriptor_set = set(descriptor_fields)
102+
view_set = set(view_fields)
103+
inline_columns = [
104+
name for name in column_names
105+
if name in descriptor_set or name in view_set
106+
]
107+
if not inline_columns:
108+
return row, values_by_name
109+
values_by_name = dict(values_by_name)
110+
for name in inline_columns:
111+
values_by_name[name] = inline_blob_value(
112+
values_by_name[name],
113+
name in descriptor_set,
114+
name in view_set,
115+
)
116+
from pypaimon.table.row.generic_row import GenericRow
117+
row = GenericRow(
118+
[values_by_name[name] for name in column_names],
119+
[field_dict[name] for name in column_names],
120+
row.get_row_kind(),
121+
)
122+
return row, values_by_name
123+
124+
93125
def value_for_arrow(value: Any, field: DataField) -> Any:
94126
if isinstance(value, Vector):
95127
return value.to_list()

0 commit comments

Comments
 (0)