blob: 567cd1618de4e40e447b5dfaff340125fa760a7e [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, Optional
import pyarrow as pa
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.write.commit_message import CommitMessage
from pypaimon.execution.python.write.file_store_write import PostponeFixedBucketFileStoreWrite
from pypaimon.execution.python.write.postpone_bucket import PythonPostponeBucketPlanner as PostponeBucketPlanner
from pypaimon.execution.python.write.table_write import PythonTableWrite
from pypaimon.execution.python.write.row_key_extractor import PostponeFixedBucketRowKeyExtractor
from pypaimon.write.row_utils import (
normalize_inline_blob_row,
require_columns,
row_to_named_values,
row_values_to_arrow_table,
)
class PythonPostponeFixedBucketWrite(PythonTableWrite):
"""Batch writer which plans postpone rows before routing new partitions."""
def initialize(
self,
table,
commit_user,
static_partition: Optional[dict] = None,
bucket_plan=None,
):
self._planner = PostponeBucketPlanner(
table,
known_num_buckets=(
bucket_plan.as_dict() if bucket_plan is not None
else {} if static_partition is not None else None
),
)
self._bucket_plan = (
bucket_plan
if bucket_plan is not None
else self._planner.current_plan()
)
self._plan_provided = bucket_plan is not None
self._pending_inputs = []
super().initialize(table, commit_user, static_partition)
def _create_file_store_write(self, commit_user):
return PostponeFixedBucketFileStoreWrite(self.table, commit_user)
def _create_row_key_extractor(self, static_partition):
return PostponeFixedBucketRowKeyExtractor(
self.table, self._bucket_plan)
def _write_partition_bucket_batch(self, partition, bucket, data):
self.file_store_write.write(
partition,
bucket,
data,
self.row_key_extractor.num_buckets(partition),
)
def _write_partition_bucket_row(
self, partition, bucket, row, values_by_name
):
self.file_store_write.write_row(
partition,
bucket,
row,
values_by_name,
self.row_key_extractor.num_buckets(partition),
)
def _buffer_input(self, data) -> bool:
if self._plan_provided:
return False
return any(
not self._bucket_plan.contains(partition)
for partition in self._planner.input_partition_stats(data)
)
def write_arrow(self, table: pa.Table, bucket=None):
if bucket is not None:
raise ValueError('Precomputed bucket writes require HASH_FIXED or HASH_DYNAMIC tables')
table = self._prepare_arrow_data(table)
if not self._buffer_input(table):
return super().write_arrow(table)
self._pending_inputs.extend(
("batch", batch) for batch in table.to_batches())
def write_arrow_batch(self, data: pa.RecordBatch, bucket=None):
if bucket is not None:
raise ValueError('Precomputed bucket writes require HASH_FIXED or HASH_DYNAMIC tables')
data = self._prepare_arrow_data(data)
if not self._buffer_input(data):
return super().write_arrow_batch(data)
self._pending_inputs.append(("batch", data))
def write_row(self, row):
if self._plan_provided:
return super().write_row(row)
values_by_name = row_to_named_values(
row, self.table.table_schema.fields)
column_names = (
self.file_store_write.write_cols
if self.file_store_write.write_cols is not None
else list(self.table.field_names)
)
require_columns(values_by_name, column_names, "write_row")
require_columns(values_by_name, self.table.partition_keys, "write_row")
from pypaimon.write.row_kind import skip_write_row
if skip_write_row(self.table, values_by_name, row.get_row_kind(), self.file_store_write.write_cols):
return
partition = tuple(
values_by_name[key] for key in self.table.partition_keys)
if self._bucket_plan.contains(partition):
return super().write_row(row)
row, values_by_name = normalize_inline_blob_row(
row,
values_by_name,
self.table.field_dict,
column_names,
CoreOptions.blob_descriptor_fields(self.table.options),
CoreOptions.blob_view_fields(self.table.options),
)
arrow_row = row_values_to_arrow_table(
values_by_name, self.table.table_schema.fields, column_names)
size = self._planner.input_partition_stats(arrow_row)[partition][1]
self._pending_inputs.append(
("row", (row, partition, size)))
def _flush_pending_inputs(self):
if not self._pending_inputs:
return
partition_stats = {}
for input_type, value in self._pending_inputs:
if input_type == "batch":
stats_by_partition = (
self._planner.input_partition_stats(value))
else:
_, partition, size = value
stats_by_partition = {partition: (1, size)}
for partition, stats in stats_by_partition.items():
rows, size = partition_stats.get(partition, (0, 0))
partition_stats[partition] = (
rows + stats[0], size + stats[1])
self._bucket_plan = self._planner.plan(
partition_stats,
)
self.row_key_extractor.with_bucket_plan(self._bucket_plan)
inputs = self._pending_inputs
self._pending_inputs = []
for input_type, value in inputs:
if input_type == "batch":
super().write_arrow_batch(value)
else:
super().write_row(value[0])
def prepare_commit(self, commit_identifier) -> List[CommitMessage]:
self._flush_pending_inputs()
return super().prepare_commit(commit_identifier)
def _distributed_write_options(self, *, overwrite=False) -> Dict[str, Any]:
if overwrite and not self._plan_provided:
return {"postpone_bucket_planner": PostponeBucketPlanner(self.table, known_num_buckets={})}
return {"postpone_bucket_planner": self._planner}
def close(self):
self._pending_inputs = []
super().close()
def abort(self):
self._pending_inputs = []
super().abort()