blob: 8aa072b54ff91537d09a763860310b0607887ac3 [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.
"""Transport existing MERGE inputs into the core operation."""
import pyarrow as pa
from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
from pypaimon.execution.native.write.native_commit import create_native_write_table, from_native_commit_messages
from pypaimon.execution.native.write.native_update import (
_dictionary_value_type, _native_row_id_table, _native_update_columns_supported,
_native_update_formats_supported, _supported_upsert_key_type,
)
from pypaimon.execution.native.write.merge_transform import encode_merge_clause
def create_native_merge_into(table, source, on, matched, not_matched, commit_user, commit_identifier):
from pypaimon.write.merge_inputs import _is_self_merge, _is_table_like, _prepare
from pypaimon.write.merge_inputs import _normalize_on, _union_update_cols
target_keys, source_keys = _normalize_on(on)
self_merge = _is_self_merge(table, source, target_keys, source_keys)
if not_matched and table.options.video_frame_fields():
return None
native_table = _native_row_id_table(table)
if native_table is None:
return None
source_schema = None
native_source = None
if _is_table_like(source) and not self_merge:
# The source can be an append or PK table; only the target requires DE.
from pypaimon.execution.native.table import (_native_blob_view_supported)
from pypaimon.table.file_store_table import FileStoreTable
if (type(source) is not FileStoreTable
or not _native_update_formats_supported(source)
or source.options.video_frame_fields()
or (source.is_primary_key_table and not source.trimmed_primary_keys)):
return None
from pypaimon.read.scan_config import _validate_scan_mode
_validate_scan_mode(source)
if not _native_blob_view_supported(source, source.field_names):
return None
# Core resolves a full snapshot or PyPaimon's existing timestamp window.
if (source.options.scan_mode() not in (
'default', 'latest', 'latest-full', 'from-snapshot',
'from-snapshot-full', 'from-timestamp', 'incremental')
or any(str(key).startswith('incremental-') and key != 'incremental-between-timestamp'
for key in source.table_schema.options)):
return None
from pypaimon.schema.data_types import PyarrowFieldParser
projection = list(dict.fromkeys(list(source.field_names) + source_keys))
source_fields = source.new_read_builder().with_projection(projection).read_type()
source_schema = PyarrowFieldParser.from_paimon_schema(source_fields)
if source.options.query_auth_enabled:
from pypaimon.execution.native.table import (_native_query_auth_supported, _native_table)
from pypaimon.execution.native.write.native_commit import _rest_catalog_supported
if not _rest_catalog_supported(source) or not _native_query_auth_supported(source):
return None
# A source is a read, not a write. Core obtains a fresh REST grant
# and requires it to be unrestricted, as Python plan_all does.
native_source = _native_table(source)
else:
native_source = create_native_write_table(source)
if native_source is None:
return None
from pypaimon.execution.native.table import (_with_native_read_snapshot)
native_source = _with_native_read_snapshot(source, native_source)
source, matched, not_matched, context = _prepare(
table, source, list(matched), list(not_matched), on, source_schema=source_schema)
if not _native_update_columns_supported(table, _union_update_cols(matched)):
return None
for target, origin in zip(target_keys, source_keys):
target_type = pa.int64() if target == '_ROW_ID' else context.full_pa_schema.field(target).type
source_type = target_type if self_merge else _dictionary_value_type(source.schema.field(origin).type)
if source_type != target_type or not _supported_upsert_key_type(target_type):
return None
on_pairs = list(zip(source_keys, target_keys))
encoded_matched = [encode_merge_clause(clause, on_pairs) for clause in matched]
encoded_not_matched = [encode_merge_clause(clause, on_pairs, not_matched=True) for clause in not_matched]
if any(clause is None for clause in encoded_matched + encoded_not_matched):
return None
builder = (native_table.new_batch_write_builder().with_commit_user(commit_user)
if commit_identifier == BATCH_COMMIT_IDENTIFIER else
native_table.new_stream_write_builder().with_commit_user(commit_user))
writer = builder.new_update()
return NativeTableMergeInto(table, writer, native_source if native_source is not None else source,
list(zip(target_keys, source_keys)),
encoded_matched, encoded_not_matched, commit_identifier)
class NativeTableMergeInto:
def __init__(self, table, writer, source, on, matched, not_matched, commit_identifier):
self.table = table
self.writer = writer
self.source = source
self.on = on
self.matched = matched
self.not_matched = not_matched
self.commit_identifier = commit_identifier
def prepare_commit(self):
# The core operation owns the snapshot, matching, action selection and staging.
# Never retry via Python after it starts or delete prepared messages on failure.
kwargs = ({} if self.commit_identifier == BATCH_COMMIT_IDENTIFIER else
dict(commit_identifier=self.commit_identifier))
return from_native_commit_messages(self.table, self.writer.merge_into(
self.source, on=self.on, when_matched=self.matched, when_not_matched=self.not_matched, **kwargs))