blob: e0c4a702e8ed933d73394caf9785612b625d1e91 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any, Dict, List, Sequence, Union
import pyarrow as pa
from pypaimon.schema.data_types import PyarrowFieldParser, is_blob_file_field
from pypaimon.table.special_fields import SpecialFields
from pypaimon.write.commit_message import CommitMessage
from pypaimon.write.row_utils import (inline_blob_value, require_columns, row_to_named_values, value_for_arrow)
RowIdInput = Union[Sequence[int], pa.Array, pa.ChunkedArray]
def normalize_row_ids(row_ids: RowIdInput):
"""Validate IDs without turning Arrow buffers into Python objects.
Keep chunk boundaries for Native transport. Only integer Arrow layouts
have a row-ID identity; floating, Boolean and untyped NULL arrays must not
be silently converted. Existing sequence callers retain integer coercion.
"""
if isinstance(row_ids, (pa.Array, pa.ChunkedArray)):
if not pa.types.is_integer(row_ids.type):
raise TypeError('_ROW_ID input must be an integer Arrow array')
if row_ids.null_count:
raise ValueError('_ROW_ID value must not be null.')
return row_ids.cast(pa.int64(), safe=True)
values = []
for row_id in row_ids:
if row_id is None:
raise ValueError('_ROW_ID value must not be null.')
values.append(int(row_id))
return values
class RowIdUpdateInputMixin:
"""Normalize Python row inputs for either update execution backend."""
def update_row_columns(
self,
row,
row_ids: List[int],
column_names: List[str],
) -> List[CommitMessage]:
return self.update_rows_columns([row], [row_ids], column_names)
def update_rows_columns(
self,
rows: List,
row_ids_by_row: List[List[int]],
column_names: List[str],
) -> List[CommitMessage]:
if not column_names:
raise ValueError("column_names cannot be empty")
column_names = list(dict.fromkeys(column_names))
if len(rows) != len(row_ids_by_row):
raise ValueError(
"rows and row_ids_by_row must have the same length: "
f"{len(rows)} != {len(row_ids_by_row)}"
)
values_by_row = [
row_to_named_values(row, self.table.table_schema.fields)
for row in rows
]
for values_by_name in values_by_row:
require_columns(values_by_name, column_names, "update_rows_columns")
row_entries = []
for values_by_name, row_ids in zip(values_by_row, row_ids_by_row):
row_entries.extend((row_id, values_by_name) for row_id in row_ids)
if not row_entries:
return []
row_entries.sort(key=lambda item: item[0])
for col_name in column_names:
if col_name not in self.table.field_names:
raise ValueError(f"Column {col_name} not found in table schema")
arrays = [
pa.array([row_id for row_id, _ in row_entries], type=pa.int64())
]
fields = [pa.field(SpecialFields.ROW_ID.name, pa.int64())]
blob_object_columns: Dict[str, List[Any]] = {}
descriptor_fields = self.table.options.blob_descriptor_fields()
view_fields = self.table.options.blob_view_fields()
for col_name in column_names:
if self._is_blob_column(col_name):
blob_object_columns[col_name] = [
values_by_name[col_name]
for _, values_by_name in row_entries
]
continue
table_field = self.table.field_dict[col_name]
arrow_field = PyarrowFieldParser.from_paimon_field(table_field)
arrays.append(
pa.array(
[
value_for_arrow(inline_blob_value(
values_by_name[col_name],
col_name in descriptor_fields, col_name in view_fields), table_field)
for _, values_by_name in row_entries
],
type=arrow_field.type,
)
)
fields.append(arrow_field)
update_data = pa.Table.from_arrays(arrays, schema=pa.schema(fields))
return self._write_row_columns(update_data, column_names, blob_object_columns)
def _is_blob_column(self, column_name: str) -> bool:
if column_name in (self.table.options.blob_descriptor_fields() | self.table.options.blob_view_fields()):
return False
for table_field in self.table.fields:
if table_field.name == column_name:
return is_blob_file_field(table_field)
return False