blob: 6819b4112646dc090f9ac6c543863944add4cc49 [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.
################################################################################
"""Select the first matching MERGE action for each Python input row."""
from typing import Dict, List, Optional, Sequence, Tuple
import pyarrow as pa
from pypaimon.write.merge_condition import _load_datafusion
from pypaimon.write.merge_types import _NormalizedClause
from pypaimon.write.merge_transform import (
cast_to_schema,
vectorized_delete_transform,
vectorized_insert_transform,
vectorized_matched_transform,
)
def _build_matched_transform(
clauses: List[_NormalizedClause],
on_map: Dict[str, str],
on_pairs: List[Tuple[str, str]],
update_cols: List[str],
row_id_name: str,
update_schema: pa.Schema,
callable_input_columns: Optional[Sequence[str]] = None,
):
prepared_clauses = []
for clause in clauses:
rewritten = None
if clause.condition is not None:
from pypaimon.write.merge_condition import (
remap_source_on_keys, rewrite_condition,
)
rewritten = remap_source_on_keys(
rewrite_condition(clause.condition), on_map,
)
prepared_clauses.append((clause.spec, rewritten, clause.delete))
_filter_batch = None
if any(r is not None for _, r, _ in prepared_clauses):
_load_datafusion()
from pypaimon.write.merge_condition import filter_batch as _filter_batch
def _transform(batch: pa.Table) -> pa.Table:
remaining = batch
parts = []
for spec, rewritten, is_delete in prepared_clauses:
if remaining.num_rows == 0:
break
if rewritten is not None:
matched = _filter_batch(
remaining, rewritten, _pre_rewritten=True,
)
else:
matched = remaining
if matched.num_rows == 0:
continue
if not is_delete:
callable_input = None
if (callable_input_columns is not None
and any(callable(value) and not isinstance(value, type)
for value in spec.values())):
callable_input = pa.Table.from_arrays(
[matched.column(f"t.{col}")
for col in callable_input_columns],
names=list(callable_input_columns),
)
parts.append(vectorized_matched_transform(
matched, spec, on_pairs,
update_cols, row_id_name,
update_schema,
callable_input=callable_input,
))
if rewritten is not None and matched.num_rows < remaining.num_rows:
not_cond = f"COALESCE(NOT ({rewritten}), TRUE)"
remaining = _filter_batch(
remaining, not_cond, _pre_rewritten=True,
)
else:
remaining = remaining.slice(0, 0)
if not parts:
return update_schema.empty_table()
return pa.concat_tables(parts)
return _transform
def _build_matched_delete_transform(
clauses: List[_NormalizedClause],
on_map: Dict[str, str],
row_id_name: str,
delete_schema: pa.Schema,
):
prepared_clauses = []
for clause in clauses:
rewritten = None
if clause.condition is not None:
from pypaimon.write.merge_condition import (
remap_source_on_keys, rewrite_condition,
)
rewritten = remap_source_on_keys(
rewrite_condition(clause.condition), on_map,
)
prepared_clauses.append((rewritten, clause.delete))
_filter_batch = None
if any(r is not None for r, _ in prepared_clauses):
_load_datafusion()
from pypaimon.write.merge_condition import filter_batch as _filter_batch
def _transform(batch: pa.Table) -> pa.Table:
remaining = batch
parts = []
for rewritten, is_delete in prepared_clauses:
if remaining.num_rows == 0:
break
if rewritten is not None:
matched = _filter_batch(
remaining, rewritten, _pre_rewritten=True,
)
else:
matched = remaining
if matched.num_rows > 0 and is_delete:
parts.append(
vectorized_delete_transform(
matched, row_id_name, delete_schema,
)
)
if rewritten is not None and matched.num_rows < remaining.num_rows:
not_cond = f"COALESCE(NOT ({rewritten}), TRUE)"
remaining = _filter_batch(
remaining, not_cond, _pre_rewritten=True,
)
else:
remaining = remaining.slice(0, 0)
if not parts:
return delete_schema.empty_table()
return pa.concat_tables(parts)
return _transform
def _build_insert_transform(
clauses: List[_NormalizedClause],
target_field_names: Sequence[str],
target_pa_schema: pa.Schema,
):
prepared_clauses = []
for clause in clauses:
rewritten = None
if clause.condition is not None:
from pypaimon.write.merge_condition import rewrite_condition
rewritten = rewrite_condition(clause.condition)
prepared_clauses.append((clause.spec, rewritten))
_filter_batch = None
if any(r is not None for _, r in prepared_clauses):
_load_datafusion()
from pypaimon.write.merge_condition import filter_batch as _filter_batch
def _transform(batch: pa.Table) -> pa.Table:
remaining = batch
parts = []
for spec, rewritten in prepared_clauses:
if remaining.num_rows == 0:
break
if rewritten is not None:
matched = _filter_batch(
remaining, rewritten, _pre_rewritten=True
)
if matched.num_rows > 0:
parts.append(
vectorized_insert_transform(
matched,
spec,
target_field_names,
target_pa_schema,
)
)
if matched.num_rows < remaining.num_rows:
not_cond = "COALESCE(NOT ({}), TRUE)".format(rewritten)
remaining = _filter_batch(
remaining, not_cond, _pre_rewritten=True
)
else:
remaining = remaining.slice(0, 0)
else:
parts.append(
vectorized_insert_transform(
remaining,
spec,
target_field_names,
target_pa_schema,
)
)
remaining = remaining.slice(0, 0)
if not parts:
return target_pa_schema.empty_table()
return cast_to_schema(pa.concat_tables(parts), target_pa_schema)
return _transform