JingsongLi commented on code in PR #9239:
URL: https://github.com/apache/paimon/pull/9239#discussion_r3789614798


##########
paimon-python/pypaimon/write/table_update.py:
##########
@@ -228,17 +228,17 @@ def _update_by_predicate(
     ) -> List[CommitMessage]:
         """Shared implementation for SQL-like ``UPDATE ... WHERE ...``.
 
-        ``predicate`` identifies the target rows. ``assignments`` maps target
-        column names to literal values. The method reads matching ``_ROW_ID``
-        values, builds an Arrow update table, then delegates to the existing
-        row-id update path.
+        ``predicate`` identifies the target rows. Assignment values may be
+        literals or callables receiving the matched rows as an Arrow table.
         """
         self._validate_predicate_update(assignments)
 
         scan_table = self._matched_update_scan_table()
         read_builder = scan_table.new_read_builder()
         if predicate is not None:
             read_builder.with_filter(predicate)
+        if predicate is not None or any(
+                callable(value) for value in assignments.values()):

Review Comment:
   **[P1] Avoid materializing every column for full-table callables**
   
   With `predicate=None`, any callable takes this branch, so even an assignment 
that reads one scalar column projects every field and `to_arrow(splits)` 
retains the whole table. Because `blob-as-descriptor` defaults to false and 
BLOB deferral is disabled for predicate/limit-free reads, this also resolves 
every BLOB payload before the callback runs. On production-scale or multimodal 
tables this makes a single-column update require unbounded driver memory and 
full BLOB I/O. Please require declared input columns and execute bounded 
batches/ranges, or reject unbounded callable updates.



##########
paimon-python/pypaimon/write/table_update.py:
##########
@@ -313,19 +312,22 @@ def _validate_predicate_update(self, assignments: 
Mapping[str, Any]):
 
     def _build_predicate_update_table(
             self,
-            row_ids,
             assignments: Mapping[str, Any],
-            row_count: int,
+            matched: pa.Table,
     ) -> pa.Table:
         table_schema = PyarrowFieldParser.from_paimon_schema(
             self.table.table_schema.fields
         )
-        arrays = [row_ids]
+        arrays = [matched[SpecialFields.ROW_ID.name]]
         fields = [pa.field(SpecialFields.ROW_ID.name, pa.int64())]
         for col, value in assignments.items():
+            if callable(value):
+                value = value(matched)
             target_field = table_schema.field(col)
             arrays.append(
-                self._assignment_to_array(value, target_field.type, row_count)
+                self._assignment_to_array(
+                    value, target_field.type, matched.num_rows

Review Comment:
   **[P2] Preserve chunked callable outputs**
   
   Arrow kernels and identity transforms over `rows[col]` normally return a 
`ChunkedArray`, but `_assignment_to_array` immediately calls 
`combine_chunks()`. That fails for otherwise valid multi-chunk string/list 
values whose aggregate 32-bit offsets exceed 2 GiB. I reproduced `ArrowInvalid: 
offset overflow` on this head with two valid list chunks totaling 2.2B 
children. Please keep/cast chunks independently and pass the chunked result 
through `Table.from_arrays`, with a large-offset regression test.



##########
paimon-python/pypaimon/write/table_update.py:
##########
@@ -252,9 +252,8 @@ def _update_by_predicate(
             return []
 
         update_table = self._build_predicate_update_table(
-            matched[SpecialFields.ROW_ID.name],
             assignments,
-            matched.num_rows,
+            matched,

Review Comment:
   **[P1] Pin the updater to the snapshot that produced `matched`**
   
   A callable can run for an arbitrary time after this table is read, but 
`TableUpdateByRowId(self.table, ...)` is created afterward and records the 
then-latest snapshot as `check_from_snapshot`. If another writer changes the 
same row/column during the callback, the callback still returns a value derived 
from the old snapshot while the newer commit is treated as the base, so the 
stale update commits without a conflict. On this head I reproduced `age: 35 -> 
concurrent 100 -> final 36`, with scan snapshot 2 but message baseline 3. 
Please pin the updater/file metadata and conflict baseline to this scan plan's 
snapshot before invoking user code, and add a concurrent-callback regression 
test.



##########
paimon-python/pypaimon/write/table_update.py:
##########
@@ -313,19 +312,22 @@ def _validate_predicate_update(self, assignments: 
Mapping[str, Any]):
 
     def _build_predicate_update_table(
             self,
-            row_ids,
             assignments: Mapping[str, Any],
-            row_count: int,
+            matched: pa.Table,
     ) -> pa.Table:
         table_schema = PyarrowFieldParser.from_paimon_schema(
             self.table.table_schema.fields
         )
-        arrays = [row_ids]
+        arrays = [matched[SpecialFields.ROW_ID.name]]
         fields = [pa.field(SpecialFields.ROW_ID.name, pa.int64())]
         for col, value in assignments.items():
+            if callable(value):
+                value = value(matched)

Review Comment:
   **[P1] Track callable read dependencies in conflict detection**
   
   The callable may read any column in `matched`, but the generated 
delta/conflict metadata contains only `assignments.keys()`. A concurrent change 
to a read dependency therefore does not conflict. Two transforms staged from 
the same snapshot—`name = f(id)` and `id = g(name)`—both committed on this head 
and left `(id=5, name='id-1')`, which neither serial order can produce. This 
remains reproducible even when the messages are forced back to the correct 
read-snapshot baseline. Please require/track callable `read_columns` and 
include them in conflict detection, or conservatively conflict on concurrent 
updates to the matched row range.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to