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 884c0b673f [python] Add vectorized splice fast path for variant_set 
root inserts (#9259)
884c0b673f is described below

commit 884c0b673ff6ab0f95a72b9146ecb672a4500973
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Aug 19 20:48:25 2026 +0800

    [python] Add vectorized splice fast path for variant_set root inserts 
(#9259)
---
 paimon-python/pypaimon/data/variant_path.py      | 399 +++++++++++++++++++++--
 paimon-python/pypaimon/tests/variant_set_test.py | 315 +++++++++++++++++-
 2 files changed, 688 insertions(+), 26 deletions(-)

diff --git a/paimon-python/pypaimon/data/variant_path.py 
b/paimon-python/pypaimon/data/variant_path.py
index 17fcbab08b..79e251e9c0 100644
--- a/paimon-python/pypaimon/data/variant_path.py
+++ b/paimon-python/pypaimon/data/variant_path.py
@@ -70,6 +70,12 @@ _INDEX_PATTERN = re.compile(r"\[(\d+)]")
 _KEY_PATTERN = re.compile(r"\.([^\.\[]+)|\['([^']+)']|\[\"([^\"]+)\"]")
 _Path = Tuple[Tuple[str, object], ...]
 _SLOW_PATH_ROWS = 64
+# Bound the dominant temporary allocation during batch structure matching.
+_STRUCTURE_MATCH_INDEX_BUDGET = 8 * 1024 * 1024
+# Bound encoded variable-width payloads retained while rebuilt rows accumulate.
+_ROOT_INSERT_SPLICE_PAYLOAD_BUDGET = 8 * 1024 * 1024
+# Bound per-row Python and NumPy temporaries for tiny payloads.
+_ROOT_INSERT_SPLICE_MAX_BATCH_ROWS = 64 * 1024
 
 
 @functools.lru_cache(maxsize=256)
@@ -251,8 +257,9 @@ def _checked_object_layout(value, pos, limit):
             value, offset_start + index * offset_width, offset_width)
         offsets.append(offset)
     sentinel = offsets[-1]
-    if ((size and (min(offsets[:-1]) != 0
-                   or len(set(offsets[:-1])) != size))
+    if ((not size and sentinel != 0)
+            or (size and (min(offsets[:-1]) != 0
+                          or len(set(offsets[:-1])) != size))
             or any(offset >= sentinel for offset in offsets[:-1])):
         _malformed("invalid object offsets")
     if size and len({
@@ -345,19 +352,20 @@ def _checked_value_size(value, pos, limit=None):
     return end - pos
 
 
-def _validate_value_field_ids(value, pos, limit, metadata_size):
+def _validate_value_field_ids(
+        value, pos, limit, metadata_size, structure_ranges=None):
     """Validate object field ids in one unedited value subtree."""
     stack = [(pos, limit)]
     while stack:
         current_pos, current_limit = stack.pop()
-        value_end = current_pos + _checked_value_size(
-            value, current_pos, current_limit)
-        if value_end != current_limit:
-            _malformed("child size does not match container offsets")
+        _require_range(current_pos, 1, current_limit)
         basic_type = value[current_pos] & 0x3
         if basic_type == _OBJECT:
-            size, id_size, id_start, data_start, offsets, _ = (
-                _checked_object_layout(value, current_pos, value_end))
+            size, id_size, id_start, data_start, offsets, value_end = (
+                _checked_object_layout(
+                    value, current_pos, current_limit))
+            if structure_ranges is not None:
+                structure_ranges.append((current_pos, data_start))
             ids = [
                 _read_unsigned(value, id_start + i * id_size, id_size)
                 for i in range(size)
@@ -368,18 +376,78 @@ def _validate_value_field_ids(value, pos, limit, 
metadata_size):
             end_by_offset = dict(zip(
                 ordered_offsets, ordered_offsets[1:]))
             for slot in range(size):
-                child_start, child_end = _checked_object_child_bounds(
-                    value, data_start, offsets, slot, end_by_offset)
-                if (value[child_start] & 0x3) in (_OBJECT, _ARRAY):
-                    stack.append((child_start, child_end))
+                child_offset = offsets[slot]
+                stack.append((
+                    data_start + child_offset,
+                    data_start + end_by_offset[child_offset],
+                ))
         elif basic_type == _ARRAY:
-            size, data_start, offsets, _ = _checked_array_layout(
-                value, current_pos, value_end)
+            size, data_start, offsets, value_end = _checked_array_layout(
+                value, current_pos, current_limit)
+            if structure_ranges is not None:
+                structure_ranges.append((current_pos, data_start))
             for index in range(size):
                 stack.append((
                     data_start + offsets[index],
                     data_start + offsets[index + 1],
                 ))
+        else:
+            value_end = current_pos + _checked_value_size(
+                value, current_pos, current_limit)
+            if structure_ranges is not None:
+                type_info = (value[current_pos] >> 2) & 0x3F
+                structure_end = current_pos + 1
+                if basic_type == _PRIMITIVE:
+                    if type_info in (_BINARY, _LONG_STR):
+                        structure_end += _U32_SIZE
+                    elif type_info in (
+                            _DECIMAL4, _DECIMAL8, _DECIMAL16):
+                        structure_end = value_end
+                structure_ranges.append((current_pos, structure_end))
+        if value_end != current_limit:
+            _malformed("child size does not match container offsets")
+
+
+def _matching_value_structures(
+        value, source_data, row_starts, metadata_size):
+    """Match equal-length rows against one validated value structure."""
+    ranges = []
+    _validate_value_field_ids(
+        value, 0, len(value), metadata_size, ranges)
+    position_count = sum(end - start for start, end in ranges)
+    index_size = np.dtype(np.int64).itemsize
+    position_bytes = position_count * index_size
+    available_bytes = _STRUCTURE_MATCH_INDEX_BUDGET - position_bytes
+    if available_bytes < index_size:
+        return None
+    max_cells = available_bytes // index_size
+    positions = np.empty(position_count, dtype=np.int64)
+    cursor = 0
+    for start, end in ranges:
+        count = end - start
+        positions[cursor:cursor + count] = np.arange(
+            start, end, dtype=np.int64)
+        cursor += count
+
+    matches = np.ones(len(row_starts), dtype=bool)
+    expected = np.frombuffer(value, dtype=np.uint8)
+    width = max(1, min(position_count, max_cells))
+    row_chunk_size = min(len(matches), max(1, max_cells // width))
+    for row_start in range(0, len(matches), row_chunk_size):
+        row_end = min(row_start + row_chunk_size, len(matches))
+        batch_matches = matches[row_start:row_end]
+        batch_starts = row_starts[row_start:row_end]
+        for start in range(0, position_count, width):
+            offsets = positions[start:start + width]
+            batch_matches &= np.all(
+                source_data[batch_starts[:, None] + offsets]
+                == expected[offsets],
+                axis=1,
+            )
+            if (start + width < position_count
+                    and not np.any(batch_matches)):
+                break
+    return matches
 
 
 def _field_slot(id_table: bytes, id_size: int, key_id: int) -> Optional[int]:
@@ -1880,6 +1948,215 @@ def _apply_edits(
     return _materialize_value(results[0])
 
 
+def _root_insert_splice_layout(value, key_id, key_name, names_by_id):
+    """Return a root layout that can splice the new field."""
+    size, id_size, id_start, data_start, first_offsets, _ = (
+        _checked_object_layout(value, 0, len(value)))
+    header = value[0]
+    type_info = (header >> 2) & 0x3F
+    large_size = ((type_info >> 4) & 0x1) != 0
+    size_width = _U32_SIZE if large_size else 1
+    offset_size = (type_info & 0x3) + 1
+    offset_start = id_start + size * id_size
+    if not large_size and size + 1 > _U8_MAX:
+        return None
+    if key_id >= 1 << (8 * id_size):
+        return None
+    ordered_offsets = sorted(first_offsets)
+    end_by_offset = dict(zip(ordered_offsets, ordered_offsets[1:]))
+    for index in range(size):
+        _checked_object_child_bounds(
+            value, data_start, first_offsets, index, end_by_offset)
+    ids = [
+        _read_unsigned(value, id_start + i * id_size, id_size)
+        for i in range(size)
+    ]
+    names = [names_by_id.get(field_id) for field_id in ids]
+    if any(name is None for name in names) or names != sorted(names):
+        return None
+    slot = sum(name < key_name for name in names)
+    return (
+        header, size, size_width, id_size, id_start, data_start,
+        offset_size, offset_start, ids, slot,
+    )
+
+
+def _encoded_payload_batches(rows, provider, global_row):
+    """Encode array-backed splice payloads within byte and row budgets."""
+    batch_start = 0
+    payloads = []
+    payload_bytes = 0
+    for index, row in enumerate(rows):
+        payload = provider.encode(
+            provider.scalar_at(global_row + int(row)))
+        if (payloads
+                and (len(payloads)
+                     >= _ROOT_INSERT_SPLICE_MAX_BATCH_ROWS
+                     or payload_bytes + len(payload)
+                     > _ROOT_INSERT_SPLICE_PAYLOAD_BUDGET)):
+            yield batch_start, index, payloads
+            batch_start = index
+            payloads = []
+            payload_bytes = 0
+        payloads.append(payload)
+        payload_bytes += len(payload)
+    if payloads:
+        yield batch_start, len(rows), payloads
+
+
+def _repeated_payload_batches(row_count, payload):
+    """Repeat a scalar payload without creating an unbounded row batch."""
+    for batch_start in range(
+            0, row_count, _ROOT_INSERT_SPLICE_MAX_BATCH_ROWS):
+        batch_end = min(
+            batch_start + _ROOT_INSERT_SPLICE_MAX_BATCH_ROWS,
+            row_count)
+        yield (
+            batch_start,
+            batch_end,
+            [payload] * (batch_end - batch_start),
+        )
+
+
+def _plan_root_insert_splice(
+        values, rows, row_starts, row_lengths, source_data,
+        key_id, key_name, names_by_id, payloads):
+    """Plan a splice and identify rows matching one root layout."""
+    layout = None
+    for row in rows:
+        layout = _root_insert_splice_layout(
+            values.view(int(row)), key_id, key_name, names_by_id)
+        if layout is not None:
+            break
+    if layout is None:
+        return None
+    (
+        header, size, size_width, id_size, id_start, data_start,
+        offset_size, offset_start, ids, slot,
+    ) = layout
+
+    widths = np.full(len(rows), size_width, dtype=np.int64)
+    ok = source_data[row_starts] == header
+    ok &= row_lengths >= data_start
+    safe_starts = np.where(ok, row_starts, 0)
+    ok &= _take_unsigned(source_data, safe_starts + 1, widths) == size
+    widths = np.full(len(rows), id_size, dtype=np.int64)
+    for index in range(size):
+        ok &= _take_unsigned(
+            source_data,
+            safe_starts + id_start + index * id_size,
+            widths,
+        ) == ids[index]
+    widths = np.full(len(rows), offset_size, dtype=np.int64)
+    sentinels = _take_unsigned(
+        source_data,
+        safe_starts + offset_start + size * offset_size,
+        widths,
+    )
+    ok &= data_start + sentinels == row_lengths
+    if size:
+        minimum = None
+        for index in range(size):
+            entry = _take_unsigned(
+                source_data,
+                safe_starts + offset_start + index * offset_size,
+                widths,
+            )
+            ok &= entry < sentinels
+            minimum = entry if minimum is None else np.minimum(
+                minimum, entry)
+        ok &= minimum == 0
+    payload_lengths = np.fromiter(
+        (len(payload) for payload in payloads), np.int64, len(payloads))
+    new_sentinels = sentinels + payload_lengths
+    ok &= new_sentinels < 1 << (8 * offset_size)
+
+    return (
+        header, size, size_width, id_size, id_start, data_start,
+        offset_size, offset_start, slot, sentinels, ok,
+    )
+
+
+def _root_insert_splice(
+        values, state, rows, row_starts, row_lengths, source_data,
+        key_id, key_name, names_by_id, payloads,
+        source_metadata_size, output_metadata_size):
+    """Splice one field into uniform root objects."""
+    plan = _plan_root_insert_splice(
+        values, rows, row_starts, row_lengths, source_data,
+        key_id, key_name, names_by_id, payloads)
+    if plan is None:
+        return None
+    (
+        header, size, size_width, id_size, id_start, data_start,
+        offset_size, offset_start, slot, sentinels, ok,
+    ) = plan
+    matching_structures = np.zeros(len(rows), dtype=bool)
+    candidates = np.flatnonzero(ok)
+    if len(candidates):
+        lengths = row_lengths[candidates]
+        order = np.argsort(lengths, kind='stable')
+        candidates = candidates[order]
+        lengths = lengths[order]
+        boundaries = np.flatnonzero(lengths[1:] != lengths[:-1]) + 1
+        for group in np.split(candidates, boundaries):
+            exemplar = int(group[0])
+            value = values.view(int(rows[exemplar]))
+            if len(group) == 1:
+                _validate_value_field_ids(
+                    value, 0, len(value), source_metadata_size)
+                matching_structures[exemplar] = True
+                continue
+            matches = _matching_value_structures(
+                value, source_data, row_starts[group],
+                source_metadata_size)
+            if matches is not None:
+                matching_structures[group[matches]] = True
+
+    if state.data is not None:
+        source_view = memoryview(state.data)
+        source_base = state.data_start
+    else:
+        source_view = values.data
+        source_base = 0
+    prefix = bytes([header]) + (size + 1).to_bytes(size_width, 'little')
+    id_bytes = key_id.to_bytes(id_size, 'little')
+    id_slot = id_start + slot * id_size
+    offset_slot = offset_start + slot * offset_size
+    sentinel_slot = offset_start + size * offset_size
+    rebuilt = {}
+    fallback_rows = []
+    for index, row in enumerate(rows):
+        row = int(row)
+        if not ok[index]:
+            fallback_rows.append(row)
+            continue
+        original = values.view(row)
+        if not matching_structures[index]:
+            _validate_value_field_ids(
+                original, 0, len(original), source_metadata_size)
+        base = int(row_starts[index]) - source_base
+        sentinel = int(sentinels[index])
+        _check_variant_sizes(
+            int(row_lengths[index]) + id_size + offset_size
+            + len(payloads[index]),
+            output_metadata_size,
+        )
+        rebuilt[row] = b''.join((
+            prefix,
+            source_view[base + id_start:base + id_slot],
+            id_bytes,
+            source_view[base + id_slot:base + offset_slot],
+            sentinel.to_bytes(offset_size, 'little'),
+            source_view[base + offset_slot:base + sentinel_slot],
+            (sentinel + len(payloads[index])).to_bytes(
+                offset_size, 'little'),
+            source_view[base + data_start:base + data_start + sentinel],
+            payloads[index],
+        ))
+    return rebuilt, fallback_rows
+
+
 def _set_chunk(chunk, values, parsed, global_row):
     parsed_paths = [parsed_path for _, parsed_path, _ in parsed]
     parent_paths = [parsed_path[:-1] for parsed_path in parsed_paths]
@@ -1939,6 +2216,8 @@ def _set_chunk(chunk, values, parsed, global_row):
         target_positions = positions[:count]
         target_limits = limits[:count]
         parent_positions = positions[count:]
+        parent_limits = limits[count:]
+        group_slow = set()
         insert_indices = []
         for index, (path, parsed_path, provider) in enumerate(parsed):
             if target_positions[index] is not None:
@@ -1968,19 +2247,96 @@ def _set_chunk(chunk, values, parsed, global_row):
         insert_keys = tuple(
             parsed[index][1][-1][1] for index in insert_indices)
         metadata_key_ids = _cached_metadata_key_ids(first_metadata)
-        source_metadata_size = (
-            len(metadata_key_ids)
+        source_metadata_size = len(metadata_key_ids)
+        rebuild_validation_size = (
+            source_metadata_size
             if any(key not in metadata_key_ids for key in insert_keys)
             else None
         )
         new_metadata, key_ids, names_by_id = _metadata_with_keys(
             first_metadata, insert_keys)
         insert_set = set(insert_indices)
+        splice_eligible = (
+            len(insert_indices) == 1
+            and len(parsed[insert_indices[0]][1]) == 1
+            and all(
+                index in insert_set
+                or parsed[index][2]._fixed_size is not None
+                for index in range(count))
+        )
+        if splice_eligible:
+            insert_index = insert_indices[0]
+            replace_indices = [
+                index for index in range(count) if index != insert_index
+            ]
+            if replace_indices:
+                _patch_planned_group(
+                    (rows, row_starts, source_data,
+                     [target_positions[index] for index in replace_indices],
+                     [target_limits[index] for index in replace_indices]),
+                    [parsed[index] for index in replace_indices],
+                    len(chunk), global_row, state, group_slow, False)
+                slow_rows |= group_slow
+            if group_slow:
+                keep = np.fromiter(
+                    (int(row) not in group_slow for row in rows),
+                    bool, len(rows))
+                live_rows = rows[keep]
+                live_starts = row_starts[keep]
+                live_lengths = parent_limits[insert_index][keep]
+            else:
+                live_rows = rows
+                live_starts = row_starts
+                live_lengths = parent_limits[insert_index]
+            provider = parsed[insert_index][2]
+            if not len(live_rows):
+                continue
+            if provider._array is None:
+                payload = payload_for(insert_index, provider, 0)
+                batches = _repeated_payload_batches(
+                    len(live_rows), payload)
+            else:
+                batches = _encoded_payload_batches(
+                    live_rows, provider, global_row)
+            key_name = parsed[insert_index][1][-1][1]
+            output_metadata_size = len(
+                new_metadata if new_metadata is not None
+                else first_metadata)
+            for batch_start, batch_end, payloads in batches:
+                batch_rows = live_rows[batch_start:batch_end]
+                spliced = _root_insert_splice(
+                    values, state, batch_rows,
+                    live_starts[batch_start:batch_end],
+                    live_lengths[batch_start:batch_end],
+                    source_data, key_ids[key_name], key_name,
+                    names_by_id, payloads, source_metadata_size,
+                    output_metadata_size)
+                if spliced is None:
+                    for row in batch_rows:
+                        row = int(row)
+                        rebuild_row(
+                            row, values.view(row), insert_set, key_ids,
+                            names_by_id, first_metadata, new_metadata,
+                            rebuild_validation_size)
+                    continue
+                rebuilt, fallback_rows = spliced
+                rebuilt_rows.update(rebuilt)
+                if new_metadata is not None:
+                    for row in rebuilt:
+                        rebuilt_metadata[row] = new_metadata
+                for row in fallback_rows:
+                    rebuild_row(
+                        row, values.view(row), insert_set, key_ids,
+                        names_by_id, first_metadata, new_metadata,
+                        rebuild_validation_size)
+            continue
         for offset_index, row in enumerate(rows):
             row = int(row)
+            if row in group_slow:
+                continue
             rebuild_row(
                 row, values.view(row), insert_set, key_ids, names_by_id,
-                first_metadata, new_metadata, source_metadata_size,
+                first_metadata, new_metadata, rebuild_validation_size,
                 [
                     None if target_positions[index] is None
                     else int(target_positions[index][offset_index])
@@ -2014,8 +2370,9 @@ def _set_chunk(chunk, values, parsed, global_row):
             insert_set.add(index)
             insert_keys.append(parsed_path[-1][1])
         metadata_key_ids = _cached_metadata_key_ids(row_metadata)
-        source_metadata_size = (
-            len(metadata_key_ids)
+        source_metadata_size = len(metadata_key_ids)
+        rebuild_validation_size = (
+            source_metadata_size
             if any(key not in metadata_key_ids for key in insert_keys)
             else None
         )
@@ -2023,7 +2380,7 @@ def _set_chunk(chunk, values, parsed, global_row):
             row_metadata, tuple(insert_keys))
         rebuild_row(
             row, view, insert_set, key_ids, names_by_id, row_metadata,
-            new_metadata, source_metadata_size)
+            new_metadata, rebuild_validation_size)
 
     if rebuilt_rows:
         state.ensure()
diff --git a/paimon-python/pypaimon/tests/variant_set_test.py 
b/paimon-python/pypaimon/tests/variant_set_test.py
index 45352e1ef8..9062f6cd1b 100644
--- a/paimon-python/pypaimon/tests/variant_set_test.py
+++ b/paimon-python/pypaimon/tests/variant_set_test.py
@@ -26,11 +26,14 @@ from pypaimon.data.generic_variant import GenericVariant, 
_check_variant_sizes
 from pypaimon.data.variant_path import (
     _apply_edits,
     _build_object_value_ordered,
+    _checked_object_layout,
     _materialize_value,
     _metadata_key_ids,
     _metadata_with_keys,
     _path_positions,
     _rebuilt_offsets,
+    _root_insert_splice,
+    _root_insert_splice_layout,
     _validate_value_field_ids,
     variant_get,
 )
@@ -541,37 +544,242 @@ class TestVariantSetFastPaths(unittest.TestCase):
         ) as slow_path, patch(
                 'pypaimon.data.variant_path._metadata_key_ids',
                 wraps=_metadata_key_ids,
-        ) as metadata_parse:
+        ) as metadata_parse, patch(
+                'pypaimon.data.variant_path._apply_edits',
+                wraps=_apply_edits,
+        ) as rebuild:
             result = variant_set(column, '$.processed', pa.scalar(True))
 
         slow_path.assert_not_called()
+        rebuild.assert_not_called()
         self.assertLessEqual(metadata_parse.call_count, 2)
         self.assertEqual(
             variant_get(result, '$.processed', pa.bool_()).to_pylist(),
             [True] * 4096,
         )
 
-    def test_insert_fuses_root_validation_with_rebuild(self):
+    def test_insert_splices_nested_root_after_validation(self):
         column = _variants([
-            {'nested': {'value': float(index)}, 'other': float(index)}
+            {
+                'nested_object': {'value': float(index)},
+                'nested_array': [{'value': float(index)}],
+                'other': float(index),
+            }
             for index in range(100)
         ])
 
         with patch(
                 'pypaimon.data.variant_path._validate_value_field_ids',
                 wraps=_validate_value_field_ids,
-        ) as subtree_validation:
+        ) as subtree_validation, patch(
+                'pypaimon.data.variant_path._apply_edits',
+                wraps=_apply_edits,
+        ) as rebuild:
             result = variant_set(column, '$.processed', pa.scalar(True))
 
-        self.assertFalse(any(
+        self.assertTrue(any(
             args[1] == 0
             for args, _ in subtree_validation.call_args_list
         ))
+        self.assertEqual(subtree_validation.call_count, 1)
+        rebuild.assert_not_called()
         self.assertEqual(
             variant_get(result, '$.processed', pa.bool_()).to_pylist(),
             [True] * 100,
         )
 
+    def test_insert_batches_multiple_nested_structures(self):
+        sequences = [0, 128, 32768]
+        column = _variants([
+            {
+                'nested': {'value': float(index)},
+                'sequence': sequences[index % len(sequences)],
+            }
+            for index in range(300)
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path._validate_value_field_ids',
+                wraps=_validate_value_field_ids,
+        ) as subtree_validation, patch(
+                'pypaimon.data.variant_path._apply_edits',
+                wraps=_apply_edits,
+        ) as rebuild:
+            result = variant_set(column, '$.processed', pa.scalar(True))
+
+        self.assertEqual(subtree_validation.call_count, len(sequences))
+        rebuild.assert_not_called()
+        self.assertEqual(
+            variant_get(result, '$.processed', pa.bool_()).to_pylist(),
+            [True] * len(column),
+        )
+
+    def test_insert_batches_all_nested_structure_lengths(self):
+        lengths = list(range(1, 13))
+        column = _variants([
+            {'nested': {'value': 'x' * length}}
+            for length in lengths
+            for _ in range(10)
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path._validate_value_field_ids',
+                wraps=_validate_value_field_ids,
+        ) as subtree_validation, patch(
+                'pypaimon.data.variant_path._apply_edits',
+                wraps=_apply_edits,
+        ) as rebuild:
+            result = variant_set(column, '$.processed', pa.scalar(True))
+
+        self.assertEqual(subtree_validation.call_count, len(lengths))
+        rebuild.assert_not_called()
+        self.assertEqual(
+            variant_get(result, '$.processed', pa.bool_()).to_pylist(),
+            [True] * len(column),
+        )
+
+    def test_insert_validates_singleton_lengths_without_batching(self):
+        lengths = list(range(1, 13))
+        column = _variants([
+            {'nested': {'value': 'x' * length}}
+            for length in lengths
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path._matching_value_structures',
+        ) as structure_match, patch(
+                'pypaimon.data.variant_path._validate_value_field_ids',
+                wraps=_validate_value_field_ids,
+        ) as subtree_validation:
+            result = variant_set(column, '$.processed', pa.scalar(True))
+
+        structure_match.assert_not_called()
+        self.assertEqual(subtree_validation.call_count, len(lengths))
+        self.assertEqual(
+            variant_get(result, '$.processed', pa.bool_()).to_pylist(),
+            [True] * len(column),
+        )
+
+    def test_insert_uses_layout_after_noncanonical_first_row(self):
+        metadata = GenericVariant.from_python({'a': 0, 'b': 0}).metadata()
+        key_ids = _metadata_key_ids(metadata)
+        a_value = _encode_scalar_to_value_bytes(1.0, pa.float64())
+        b_value = _encode_scalar_to_value_bytes(2.0, pa.float64())
+        noncanonical = _build_object_value_ordered([
+            (key_ids['b'], b_value),
+            (key_ids['a'], a_value),
+        ])
+        canonical = _build_object_value_ordered([
+            (key_ids['a'], a_value),
+            (key_ids['b'], b_value),
+        ])
+        column = GenericVariant.to_arrow_array([
+            GenericVariant(noncanonical, metadata),
+            *[GenericVariant(canonical, metadata) for _ in range(99)],
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path._apply_edits',
+                wraps=_apply_edits,
+        ) as rebuild:
+            result = variant_set(column, '$.processed', pa.scalar(True))
+
+        self.assertEqual(rebuild.call_count, 1)
+        self.assertEqual(
+            variant_get(result, '$.processed', pa.bool_()).to_pylist(),
+            [True] * len(column),
+        )
+
+    def test_variable_insert_bounds_encoded_payload_memory(self):
+        column = _variants([
+            {'value': float(index)}
+            for index in range(10)
+        ])
+        tags = pa.array(['x' * 10] * len(column))
+
+        with patch(
+                'pypaimon.data.variant_path.'
+                '_ROOT_INSERT_SPLICE_PAYLOAD_BUDGET',
+                24,
+        ), patch(
+                'pypaimon.data.variant_path._root_insert_splice',
+                wraps=_root_insert_splice,
+        ) as splice:
+            result = variant_set(column, '$.tag', tags)
+
+        self.assertGreater(splice.call_count, 1)
+        for call in splice.call_args_list:
+            self.assertLessEqual(
+                sum(len(payload) for payload in call.args[9]),
+                24,
+            )
+        self.assertEqual(
+            variant_get(result, '$.tag', pa.string()).to_pylist(),
+            tags.to_pylist(),
+        )
+
+    def test_small_variable_insert_bounds_payload_batch_rows(self):
+        column = _variants([
+            {'value': float(index)}
+            for index in range(10)
+        ])
+        tags = pa.array([''] * len(column))
+
+        with patch(
+                'pypaimon.data.variant_path.'
+                '_ROOT_INSERT_SPLICE_MAX_BATCH_ROWS',
+                3,
+        ), patch(
+                'pypaimon.data.variant_path._root_insert_splice',
+                wraps=_root_insert_splice,
+        ) as splice:
+            result = variant_set(column, '$.tag', tags)
+
+        self.assertGreater(splice.call_count, 1)
+        for call in splice.call_args_list:
+            self.assertLessEqual(len(call.args[9]), 3)
+        self.assertEqual(
+            variant_get(result, '$.tag', pa.string()).to_pylist(),
+            tags.to_pylist(),
+        )
+
+    def test_scalar_insert_bounds_splice_batch_rows(self):
+        column = _variants([
+            {'value': float(index)}
+            for index in range(10)
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path.'
+                '_ROOT_INSERT_SPLICE_MAX_BATCH_ROWS',
+                3,
+        ), patch(
+                'pypaimon.data.variant_path._root_insert_splice',
+                wraps=_root_insert_splice,
+        ) as splice:
+            result = variant_set(column, '$.processed', pa.scalar(True))
+
+        self.assertEqual(splice.call_count, 4)
+        for call in splice.call_args_list:
+            self.assertLessEqual(len(call.args[9]), 3)
+        self.assertEqual(
+            variant_get(result, '$.processed', pa.bool_()).to_pylist(),
+            [True] * len(column),
+        )
+
+    def test_ineligible_splice_layout_skips_child_validation(self):
+        value = _build_object_value([
+            (0, _encode_scalar_to_value_bytes(1.0, pa.float64())),
+        ])
+
+        with patch(
+                'pypaimon.data.variant_path._checked_object_child_bounds',
+                side_effect=AssertionError(
+                    "ineligible layout validated children"),
+        ):
+            self.assertIsNone(_root_insert_splice_layout(
+                value, 256, 'new', {0: 'value'}))
+
     def test_insert_validates_deep_unmodified_sibling_iteratively(self):
         metadata = GenericVariant.from_python(
             {'sibling': [], 'target': {}}).metadata()
@@ -708,6 +916,46 @@ class TestVariantSetErrors(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
             variant_set(column, '$.processed', pa.scalar(True))
 
+    def test_root_splice_rejects_empty_object_with_orphan_data(self):
+        empty = GenericVariant.from_python({})
+        corrupt = bytearray(empty.value())
+        corrupt[-1] = 1
+        corrupt.append(0)
+        column = GenericVariant.to_arrow_array([
+            GenericVariant(bytes(corrupt), empty.metadata()),
+        ])
+
+        with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
+            variant_set(column, '$.processed', pa.scalar(True))
+
+    def test_root_splice_validates_nested_values_when_metadata_reused(self):
+        metadata = GenericVariant.from_python({
+            'nested': {'bad': None, 'target': 0.0},
+            'new': True,
+        }).metadata()
+        key_ids = _metadata_key_ids(metadata)
+        malformed_null = (
+            _encode_scalar_to_value_bytes(None, pa.null()) + b'\x00')
+        nested = _build_object_value([
+            (key_ids['bad'], malformed_null),
+            (
+                key_ids['target'],
+                _encode_scalar_to_value_bytes(1.0, pa.float64()),
+            ),
+        ])
+        root = _build_object_value([
+            (key_ids['nested'], nested),
+        ])
+        column = GenericVariant.to_arrow_array([
+            GenericVariant(root, metadata),
+        ])
+
+        with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
+            variant_set(column, {
+                '$.nested.target': pa.scalar(2.0),
+                '$.new': pa.scalar(True),
+            })
+
     def test_rejects_unknown_field_id_on_insert(self):
         metadata = GenericVariant.from_python({'value': 0}).metadata()
         orphan = _build_object_value([
@@ -753,6 +1001,34 @@ class TestVariantSetErrors(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
             variant_set(column, '$.child.new', pa.scalar(True))
 
+    def test_root_splice_rejects_nested_unknown_field_id(self):
+        metadata = GenericVariant.from_python({'sibling': {}}).metadata()
+        key_ids = _metadata_key_ids(metadata)
+        valid_sibling = _build_object_value([
+            (
+                key_ids['sibling'],
+                _encode_scalar_to_value_bytes(1.0, pa.float64()),
+            ),
+        ])
+        corrupt_sibling = _build_object_value([
+            (
+                len(key_ids),
+                _encode_scalar_to_value_bytes(2.0, pa.float64()),
+            ),
+        ])
+        corrupt_root = _build_object_value([
+            (key_ids['sibling'], corrupt_sibling),
+        ])
+        column = GenericVariant.to_arrow_array([
+            GenericVariant(_build_object_value([
+                (key_ids['sibling'], valid_sibling),
+            ]), metadata),
+            GenericVariant(corrupt_root, metadata),
+        ])
+
+        with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
+            variant_set(column, '$.new', pa.scalar(True))
+
     def test_rejects_duplicate_source_field_id(self):
         metadata = GenericVariant.from_python({'value': 0}).metadata()
         corrupt = _build_object_value([
@@ -782,6 +1058,35 @@ class TestVariantSetErrors(unittest.TestCase):
                         ValueError, "MALFORMED_VARIANT"):
                     updater(column, '$.a', pa.scalar(9.0))
 
+    def test_root_splice_rejects_duplicate_peer_offsets(self):
+        valid = GenericVariant.from_python({'a': 1.0, 'b': 2.0})
+        corrupt = bytearray(valid.value())
+        size, id_size, id_start, _, _, _ = _checked_object_layout(
+            corrupt, 0, len(corrupt))
+        offset_size = ((corrupt[0] >> 2) & 0x3) + 1
+        offset_start = id_start + size * id_size
+        corrupt[offset_start + offset_size:
+                offset_start + 2 * offset_size] = (
+            0).to_bytes(offset_size, 'little')
+        column = GenericVariant.to_arrow_array([
+            valid, GenericVariant(bytes(corrupt), valid.metadata()),
+        ])
+
+        with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
+            variant_set(column, '$.new', pa.scalar(True))
+
+    def test_root_splice_enforces_value_size_limit(self):
+        variant = GenericVariant.from_python({'padding': 'x' * 100})
+        column = GenericVariant.to_arrow_array([variant])
+
+        with patch(
+                'pypaimon.data.generic_variant._SIZE_LIMIT',
+                len(variant.value()) + 2,
+        ):
+            with self.assertRaisesRegex(
+                    ValueError, 'VARIANT_CONSTRUCTOR_SIZE_LIMIT'):
+                variant_set(column, '$.new', pa.scalar(True))
+
     def test_rejects_truncated_child_offsets(self):
         valid = GenericVariant.from_python({'a': 1.0, 'b': 2.0})
         truncated = _build_object_value([

Reply via email to