| # 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. | |
| """Optional native row-ID updates for batch data-evolution tables.""" | |
| import json | |
| from datetime import date, datetime, time | |
| import pyarrow as pa | |
| import pyarrow.compute as pc | |
| from pypaimon.execution.native.table import _native_query_auth_supported | |
| from pypaimon.execution.native.runtime import native_runtime_available | |
| from pypaimon.schema.arrow_schema import arrow_schemas_compatible | |
| from pypaimon.schema.data_types import MapType, PyarrowFieldParser, RowType, VectorType, is_blob_file_field | |
| from pypaimon.common.options.core_options import ChangelogProducer | |
| from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER | |
| from pypaimon.table.file_store_table import FileStoreTable | |
| from pypaimon.execution.native.write.native_commit import ( | |
| create_native_write_table, from_native_commit_messages, | |
| ) | |
| from pypaimon.write.row_input import RowIdUpdateInputMixin, normalize_row_ids | |
| from pypaimon.write.row_utils import _contains_blob_value, value_for_arrow | |
| def _native_update_formats_supported(table): | |
| return (table.options.file_format() == 'parquet' | |
| and (not table.options.with_vector_format() or table.options.vector_file_format() == 'parquet' | |
| or not any(isinstance(field.type, VectorType) for field in table.table_schema.fields))) | |
| def _native_update_table(table, *, allow_changelog=False): | |
| """Resolve common native update prerequisites before preparing changes.""" | |
| if (type(table) is not FileStoreTable | |
| or not table.options.native_write_enabled() | |
| or not _native_update_formats_supported(table) | |
| or (not allow_changelog and table.options.changelog_producer() != ChangelogProducer.NONE) | |
| or not native_runtime_available() | |
| or not _native_query_auth_supported(table)): | |
| return None | |
| native_table = create_native_write_table(table) | |
| if native_table is not None: | |
| from pypaimon.execution.native.table import _with_native_read_snapshot | |
| native_table = _with_native_read_snapshot(table, native_table) | |
| return native_table | |
| def _native_row_id_table(table): | |
| """Row-level operations additionally require a data-evolution table.""" | |
| if (type(table) is not FileStoreTable | |
| or not table.options.data_evolution_enabled() | |
| or not table.options.row_tracking_enabled() | |
| or table.is_primary_key_table): | |
| return None | |
| return _native_update_table(table) | |
| def create_native_update(table, commit_user, columns): | |
| """Use the public core updater for direct and grouped row-ID updates.""" | |
| if not _native_update_columns_supported(table, columns): | |
| return None | |
| native_table = _native_row_id_table(table) | |
| if native_table is None: | |
| return None | |
| writer = (native_table.new_batch_write_builder() | |
| .with_commit_user(commit_user) | |
| .new_update()) | |
| if columns is not None: | |
| writer.with_update_type(columns) | |
| return NativeBatchTableUpdate(table, writer) | |
| def create_native_update_by_row_id(table, commit_user, commit_identifier, plan=None): | |
| """Create a core updater sharing one snapshot across incremental calls.""" | |
| # Columns are selected per call; one core updater pins the snapshot and | |
| # tracks overlaps across Parquet and dedicated Blob updates. | |
| if not _native_update_columns_supported(table, None): | |
| return None | |
| native_table = _native_row_id_table( | |
| table._copy_with_snapshot(plan.table._read_snapshot) if plan is not None else table) | |
| if native_table is None: | |
| return None | |
| if commit_identifier == BATCH_COMMIT_IDENTIFIER: | |
| update = native_table.new_batch_write_builder().with_commit_user(commit_user).new_update() | |
| else: | |
| update = native_table.new_stream_write_builder().with_commit_user(commit_user).new_update() | |
| if plan is not None: | |
| from pypaimon.execution.native.write.row_id_plan import native_plan | |
| update.with_plan(native_plan(plan.table, plan.plan)) | |
| writer = (update.new_update_by_row_id() if commit_identifier == BATCH_COMMIT_IDENTIFIER | |
| else update.new_update_by_row_id(commit_identifier)) | |
| return NativeTableUpdateByRowId( | |
| table, commit_user, commit_identifier, writer) | |
| def create_native_shard_updator(table, commit_user, projection, columns, shard, shard_count, stream=False): | |
| """Core owns whole-file sharding, the captured reader and positional updates.""" | |
| if not _native_update_columns_supported(table, columns): | |
| return None | |
| native_table = _native_row_id_table(table) | |
| if native_table is None: | |
| return None | |
| builder = (native_table.new_stream_write_builder().with_commit_user(commit_user) if stream else | |
| native_table.new_batch_write_builder().with_commit_user(commit_user)) | |
| update = builder.new_update() | |
| read_builder = table.new_read_builder() | |
| if projection is not None: | |
| read_builder.with_projection(projection) | |
| if columns is not None: | |
| update.with_update_type(columns) | |
| read = read_builder.new_read() | |
| writer = update.new_shard_updator( | |
| shard, shard_count, read_type=json.dumps(RowType(True, read_builder.read_type()).to_dict())) | |
| return NativeShardTableUpdator(table, read, writer) | |
| class NativeShardTableUpdator: | |
| """Transport Arrow batches and commit messages for a core shard updater.""" | |
| def __init__(self, table, read, writer): | |
| self._table = table | |
| self._writer = writer | |
| self._read = read | |
| def arrow_reader(self): | |
| from pypaimon.execution.native.read.table_read import NativeTableRead | |
| from pypaimon.read.read_support import _ClosableArrowBatchReader | |
| schema = self._read._output_arrow_schema() | |
| batches = NativeTableRead(self._read).convert_batches(self._writer.arrow_reader(), schema) | |
| return _ClosableArrowBatchReader(pa.ipc.RecordBatchReader.from_batches(schema, batches), batches) | |
| def update_by_arrow_batch(self, data): | |
| self._writer.update_by_arrow_batch(data) | |
| def prepare_commit(self): | |
| return from_native_commit_messages(self._table, self._writer.prepare_commit()) | |
| def close(self): | |
| self._writer.close() | |
| def _supported_upsert_key_type(data_type): | |
| return any(check(data_type) for check in ( | |
| pa.types.is_boolean, pa.types.is_integer, pa.types.is_string, | |
| pa.types.is_large_string, pa.types.is_binary, pa.types.is_large_binary, | |
| pa.types.is_fixed_size_binary, pa.types.is_date, pa.types.is_decimal, | |
| pa.types.is_time, pa.types.is_timestamp, | |
| pa.types.is_floating, | |
| )) | |
| def _dictionary_value_type(data_type): | |
| while pa.types.is_dictionary(data_type): | |
| data_type = data_type.value_type | |
| return data_type | |
| def _native_upsert_key_type_compatible(source, target): | |
| """Qualify layouts; exact values and conversion bounds belong to core.""" | |
| dictionary = pa.types.is_dictionary(source) | |
| source = _dictionary_value_type(source) | |
| if dictionary: | |
| # Python's take kernel can select dictionary indices even when it | |
| # cannot take the underlying view/run-end values directly. These | |
| # layouts are therefore valid existing matched-upsert inputs. | |
| while getattr(pa.types, 'is_run_end_encoded', lambda _: False)(source): | |
| source = _dictionary_value_type(source.value_type) | |
| if getattr(pa.types, 'is_string_view', lambda _: False)(source): | |
| source = pa.string() | |
| elif getattr(pa.types, 'is_binary_view', lambda _: False)(source): | |
| source = pa.binary() | |
| if pa.types.is_union(source): | |
| # A Union can contain unused children of unrelated types. Qualify a | |
| # possible match here; core validates only the active values exactly. | |
| return any(_native_upsert_key_type_compatible(field.type, target) for field in source) | |
| if pa.types.is_null(source): | |
| return True | |
| def numeric(data_type): | |
| return (pa.types.is_integer(data_type) or pa.types.is_floating(data_type) | |
| or pa.types.is_decimal(data_type)) | |
| if numeric(source) and numeric(target): | |
| return True | |
| for family in (pa.types.is_date, pa.types.is_time): | |
| if family(source) and family(target): | |
| return True | |
| def binary(data_type): | |
| return (pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type) | |
| or pa.types.is_fixed_size_binary(data_type)) | |
| if binary(source) and binary(target): | |
| return True | |
| if pa.types.is_timestamp(source) and pa.types.is_timestamp(target): | |
| return (source.tz is None) == (target.tz is None) | |
| return arrow_schemas_compatible( | |
| pa.schema([pa.field('key', source)]), pa.schema([pa.field('key', target)]), | |
| check_top_level_nullability=False, allow_binary_compatibility=True) | |
| def _native_upsert_key_compatible(source, target): | |
| if _native_upsert_key_type_compatible(source.type, target): | |
| return True | |
| def nulls(array): | |
| if pa.types.is_dictionary(array.type): | |
| # Index validity alone misses a selected NULL dictionary value. | |
| # Select only its validity bitmap, without decoding key payloads. | |
| return pc.fill_null(pc.take(nulls(array.dictionary), array.indices), True) | |
| if pa.types.is_union(array.type): | |
| # Unions have no validity bitmap. Only the active child's NULL | |
| # counts; inactive children can contain unrelated non-NULL values. | |
| # Read the parent buffers at its offset because PyArrow's union | |
| # type_codes/offsets accessors can omit the offset of a slice. | |
| buffers = array.buffers() | |
| codes = pa.Array.from_buffers(pa.int8(), len(array), [None, buffers[1]], offset=array.offset) | |
| offsets = (pa.Array.from_buffers(pa.int32(), len(array), [None, buffers[2]], offset=array.offset) | |
| if array.type.mode == 'dense' else None) | |
| result = pc.fill_null(pa.nulls(len(array), type=pa.bool_()), False) | |
| for index, code in enumerate(array.type.type_codes): | |
| active = pc.equal(codes, code) | |
| child_nulls = nulls(array.field(index)) | |
| if offsets is not None: | |
| child_nulls = pc.take(child_nulls, pc.if_else(active, offsets, None)) | |
| result = pc.if_else(active, child_nulls, result) | |
| return result | |
| return array.is_null() | |
| try: | |
| chunks = source.chunks if isinstance(source, pa.ChunkedArray) else [source] | |
| for chunk in chunks: | |
| if chunk.null_count != len(chunk) and not pc.all(nulls(chunk)).as_py(): | |
| return False | |
| # Keep Python's existing input contract. In particular, bare view | |
| # and run-end inputs have no take kernel on supported PyArrow. | |
| pc.take(chunk, pa.array([], type=pa.int64())) | |
| return True | |
| except (pa.ArrowInvalid, pa.ArrowNotImplementedError, TypeError): | |
| return False | |
| def _native_update_columns_supported(table, columns): | |
| """Core writes Parquet columns and Java-compatible sparse Blob deltas.""" | |
| if type(table) is not FileStoreTable: | |
| return False | |
| names = set(table.field_names if columns is None else columns) | |
| return not names.intersection(table.options.video_frame_fields()) | |
| def _temporal_row_key_supported(value, data_type): | |
| """Qualify row constructors without changing temporal matching semantics.""" | |
| if value is None: | |
| return True | |
| if pa.types.is_date(data_type): | |
| # A datetime is distinct from a date even at midnight. Arrow's DATE | |
| # constructor would discard this distinction before key matching. | |
| return isinstance(value, date) and not isinstance(value, datetime) | |
| if pa.types.is_timestamp(data_type): | |
| if not isinstance(value, datetime): | |
| return False | |
| offset = value.utcoffset() | |
| if (offset is not None) != (data_type.tz is not None): | |
| return False | |
| nanos = value.microsecond * 1000 + getattr(value, 'nanosecond', 0) | |
| if offset is not None: | |
| nanos -= offset.microseconds * 1000 | |
| elif pa.types.is_time(data_type): | |
| if not isinstance(value, time): | |
| return False | |
| if value.utcoffset() is not None: | |
| return False | |
| nanos = value.microsecond * 1000 | |
| else: | |
| return True | |
| # Compare precision, not datetime objects: equivalent instants expressed | |
| # with different timezones or DST folds must keep the existing core path. | |
| resolution = {'s': 1000000000, 'ms': 1000000, 'us': 1000, 'ns': 1}[data_type.unit] | |
| return nanos % resolution == 0 | |
| def _upsert_row_batches(rows, fields, schema, table=None, blob_rows=None, key_names=()): | |
| """Encode consecutive row shapes without padding absent fields with NULL. | |
| Keep source order, including duplicates across shapes. Key matching, | |
| last-write-wins and matched/append column validation belong to Rust core. | |
| """ | |
| encoded_keys = {} | |
| for name in key_names: | |
| key_type = schema.field(name).type | |
| if any(not _temporal_row_key_supported(row[name], key_type) for row in rows): | |
| return None | |
| if not any(check(key_type) for check in ( | |
| pa.types.is_integer, pa.types.is_floating, pa.types.is_decimal, | |
| pa.types.is_boolean, pa.types.is_string, pa.types.is_large_string, | |
| pa.types.is_binary, pa.types.is_large_binary, pa.types.is_fixed_size_binary)): | |
| continue | |
| encoded = pa.array([row[name] for row in rows], type=key_type) | |
| # Row constructors can change scalar identity before core sees a key. | |
| # Qualify all keys before encoding any payload: a later unsafe row | |
| # must not fall back after an earlier Blob input has been consumed. | |
| if any(row[name] != encoded[index].as_py() for index, row in enumerate(rows)): | |
| return None | |
| encoded_keys[name] = encoded | |
| batches = [] | |
| start = 0 | |
| while start < len(rows): | |
| names = set(rows[start]) | |
| if not names <= set(schema.names): | |
| raise ValueError('upsert row fields must be in the table schema') | |
| end = start + 1 | |
| while end < len(rows) and set(rows[end]) == names: | |
| end += 1 | |
| selected = [field for field in fields if field.name in names] | |
| batch_schema = pa.schema([schema.field(field.name) for field in selected]) | |
| if blob_rows is not None: | |
| batch = blob_rows.to_batch(table, rows[start:end], batch_schema.names) | |
| else: | |
| batch = pa.RecordBatch.from_pydict({ | |
| field.name: (encoded_keys[field.name].slice(start, end - start) if field.name in encoded_keys else | |
| [value_for_arrow(row[field.name], field) for row in rows[start:end]]) | |
| for field in selected | |
| }, schema=batch_schema) | |
| batches.append(batch) | |
| start = end | |
| return batches | |
| def create_native_upsert(table, commit_user, data, keys, columns): | |
| """Prepare one core upsert from Arrow columns or named row values.""" | |
| if table.options.video_frame_fields() or not _native_update_columns_supported(table, columns): | |
| # An upsert can append unmatched rows; packed video writing is still | |
| # provided by the Python writer. | |
| return None | |
| native_table = _native_row_id_table(table) | |
| if native_table is None: | |
| return None | |
| fields = table.table_schema.fields | |
| schema = PyarrowFieldParser.from_paimon_schema(fields) | |
| blob_rows = None | |
| if any(not _supported_upsert_key_type(schema.field(key).type) for key in set(keys + table.partition_keys)): | |
| return None | |
| if not isinstance(data, pa.Table): | |
| if not columns or not data: | |
| return None | |
| if any(_contains_blob_value(value) for row in data for value in row.values()) or any( | |
| is_blob_file_field(field) and isinstance(field.type, MapType) and field.name in row | |
| for field in fields for row in data): | |
| from pypaimon.execution.native.write.native_blob_rows import NativeBlobRows | |
| blob_rows = NativeBlobRows() | |
| data = _upsert_row_batches( | |
| data, fields, schema, table, blob_rows, list(dict.fromkeys(keys + table.partition_keys))) | |
| if data is None: | |
| return None | |
| else: | |
| if (len(data.column_names) != len(set(data.column_names)) | |
| or not set(data.column_names) <= set(schema.names)): | |
| return None | |
| # Keys need a shared representation before matching. Core selects | |
| # surviving payloads before applying row-ID update or append rules; | |
| # validating all payload types here would reject valid matched updates. | |
| key_names = list(dict.fromkeys(keys + table.partition_keys)) | |
| for name in key_names: | |
| field = data.schema.field(name) | |
| if pa.types.is_null(field.type): | |
| # An untyped NULL has the same match identity as a typed NULL. | |
| # Give core the declared key layout without casting payloads. | |
| data = data.set_column( | |
| data.schema.get_field_index(name), field.with_type(schema.field(name).type), | |
| pa.nulls(data.num_rows, type=schema.field(name).type)) | |
| if not all(_native_upsert_key_compatible(data.column(name), schema.field(name).type) | |
| for name in key_names): | |
| return None | |
| writer = (native_table.new_batch_write_builder() | |
| .with_commit_user(commit_user) | |
| .new_update() | |
| .with_update_type(columns)) | |
| if blob_rows is not None: | |
| writer.with_blob_uri_reader_factory(blob_rows) | |
| return NativeTableUpsert(table, writer, keys, data, blob_rows) | |
| def create_native_predicate_update(table, commit_user, predicate, columns=None): | |
| """Prepare a public core operation before any assignment can run.""" | |
| if not _native_update_columns_supported(table, columns): | |
| return None | |
| native_table = _native_row_id_table(table) | |
| if native_table is None: | |
| return None | |
| from pypaimon.execution.native.read.builder import _predicate_to_native | |
| native_predicate = None if predicate is None else _predicate_to_native(predicate) | |
| if native_predicate is not None: | |
| # Check predicate translation while fallback is still safe. Planning, | |
| # reading and callback execution belong to the core operation below. | |
| native_table.new_read_builder().with_filter(native_predicate) | |
| writer = (native_table.new_batch_write_builder() | |
| .with_commit_user(commit_user).new_update()) | |
| return NativePredicateTableUpdate(table, writer, native_predicate) | |
| def create_native_predicate_delete(table, commit_user, predicate): | |
| """Delegate matching and partition metadata deletion to the core updater.""" | |
| from pypaimon.execution.native.read.builder import _predicate_to_native | |
| if type(table) is not FileStoreTable: | |
| return None | |
| def canonical_paths(node): | |
| if node.field_path and node.field_path != [node.field]: | |
| return False | |
| return (all(canonical_paths(child) for child in (node.literals or [])) | |
| if node.method in ('and', 'or') else True) | |
| # Core selects whole-partition metadata deletes for append/PK tables too; | |
| # only the row-level branch requires row tracking and deletion vectors. | |
| from pypaimon.write.update_validation import predicate_fields | |
| fields = set() if predicate is None else predicate_fields(predicate) | |
| partition_delete = (table.is_primary_key_table and bool(fields) | |
| and fields.issubset(table.partition_keys) and canonical_paths(predicate)) | |
| # Partition deletion removes complete file/index metadata. It does not | |
| # produce row-level changes, even when the PK writer emits changelogs. | |
| # Core still validates the translated predicate and selects this fast path. | |
| native_table = _native_update_table(table, allow_changelog=partition_delete) | |
| if native_table is None: | |
| return None | |
| native_predicate = None if predicate is None else _predicate_to_native(predicate) | |
| if native_predicate is not None: | |
| # Resolve all fields/operators before selecting the mutating operation. | |
| native_table.new_read_builder().with_filter(native_predicate) | |
| writer = (native_table.new_batch_write_builder() | |
| .with_commit_user(commit_user).new_update()) | |
| return NativePredicateTableUpdate(table, writer, native_predicate) | |
| def create_native_delete(table, commit_user, plan=None): | |
| """Select Rust's deletion-vector writer for supported batch deletes.""" | |
| if not table.options.deletion_vectors_enabled(False): | |
| return None | |
| native_table = _native_row_id_table( | |
| table._copy_with_snapshot(plan.table._read_snapshot) if plan is not None else table) | |
| if native_table is None: | |
| return None | |
| writer = (native_table.new_batch_write_builder() | |
| .with_commit_user(commit_user) | |
| .new_update()) | |
| if plan is not None: | |
| from pypaimon.execution.native.write.row_id_plan import native_plan | |
| writer.with_plan(native_plan(plan.table, plan.plan)) | |
| return NativeBatchTableUpdate(table, writer) | |
| def create_native_row_id_delete(table, plan=None): | |
| """Adapt the direct deletion API to the existing core table updater.""" | |
| operation = create_native_delete(table, table.new_batch_write_builder().commit_user, plan) | |
| return None if operation is None else NativeTableDeleteByRowId(operation) | |
| class NativeTableDeleteByRowId: | |
| def __init__(self, operation): | |
| self._operation = operation | |
| def delete(self, row_ids): | |
| return self._operation.delete_by_row_id(row_ids) | |
| def _raise_native_row_id_error(error): | |
| detail = str(error) | |
| if 'duplicate UPDATE operations' in detail: | |
| raise ValueError('duplicate _ROW_ID: ' + detail) from error | |
| if 'No file found for _ROW_ID' in detail: | |
| raise ValueError( | |
| detail + ' does not belong to any valid range') from error | |
| raise error | |
| class NativeBatchTableUpdate: | |
| """Wrap public core operations and decode their commit messages.""" | |
| def __init__(self, table, writer): | |
| self.table = table | |
| self.writer = writer | |
| def update_by_arrow_with_row_id(self, data: pa.Table): | |
| try: | |
| messages = self.writer.update_by_arrow_with_row_id(data) | |
| except ValueError as error: | |
| _raise_native_row_id_error(error) | |
| return from_native_commit_messages(self.table, messages) | |
| def update_by_arrow_batches_with_row_id(self, tables): | |
| try: | |
| messages = self.writer.update_by_arrow_batches_with_row_id(tables) | |
| except ValueError as error: | |
| _raise_native_row_id_error(error) | |
| return from_native_commit_messages(self.table, messages) | |
| def delete_by_row_id(self, row_ids): | |
| ids = normalize_row_ids(row_ids) | |
| try: | |
| messages = self.writer.delete_by_row_id(ids) | |
| except ValueError as error: | |
| _raise_native_row_id_error(error) | |
| return from_native_commit_messages(self.table, messages) | |
| class NativeTableUpdateByRowId(RowIdUpdateInputMixin): | |
| """Adapt Python row inputs around the core incremental Arrow updater.""" | |
| def __init__(self, table, commit_user, commit_identifier, writer): | |
| self.table = table | |
| self.commit_user = commit_user | |
| self.commit_identifier = commit_identifier | |
| self.writer = writer | |
| @property | |
| def commit_messages(self): | |
| return from_native_commit_messages(self.table, self.writer.commit_messages) | |
| def update_columns(self, data, column_names): | |
| try: | |
| messages = self.writer.update_columns(data, column_names) | |
| except ValueError as error: | |
| _raise_native_row_id_error(error) | |
| return from_native_commit_messages(self.table, messages) | |
| def _write_row_columns(self, data, column_names, blob_object_columns): | |
| # Row input normalization happens in the public Python API; physical | |
| # field layouts and Blob references are handled by the core updater. | |
| if not blob_object_columns: | |
| return self.update_columns(data, column_names) | |
| from pypaimon.execution.native.write.native_blob_rows import NativeBlobRows | |
| blobs = NativeBlobRows() | |
| for name, values in blob_object_columns.items(): | |
| field = self.table.field_dict[name] | |
| arrow_field = PyarrowFieldParser.from_paimon_field(field) | |
| if isinstance(field.type, MapType): | |
| # Arrow Map disallows NULL keys; Java's Blob map records allow | |
| # one. Core accepts row entries as a list of nullable-key structs. | |
| entries = pa.struct([pa.field('key', arrow_field.type.key_type), | |
| pa.field('value', arrow_field.type.item_type)]) | |
| arrow_field = pa.field(name, pa.list_(entries), nullable=arrow_field.nullable) | |
| data = data.append_column(arrow_field, pa.array( | |
| [blobs.encode(value, field.type) for value in values], type=arrow_field.type)) | |
| self.writer.with_blob_uri_reader_factory(blobs) | |
| try: | |
| return self.update_columns(data, column_names) | |
| finally: | |
| self.writer.with_blob_uri_reader_factory(None) | |
| class NativeTableUpsert: | |
| """Submit Arrow tables or row-shape batches to the core Rust upsert writer.""" | |
| def __init__(self, table, writer, keys, data, blob_rows=None): | |
| self.table = table | |
| self.writer = writer | |
| self.keys = keys | |
| self.data = data | |
| self.blob_rows = blob_rows | |
| def upsert(self): | |
| try: | |
| return from_native_commit_messages( | |
| self.table, | |
| self.writer.upsert_by_arrow_with_key(self.data, self.keys)) | |
| finally: | |
| if self.blob_rows is not None: | |
| self.blob_rows.clear() | |
| class NativePredicateTableUpdate: | |
| """Convert operation inputs and commit messages around the core updater.""" | |
| def __init__(self, table, writer, predicate): | |
| self.table = table | |
| self.writer = writer | |
| self.predicate = predicate | |
| def update(self, assignments, read_columns): | |
| return from_native_commit_messages( | |
| self.table, | |
| self.writer.update_by_predicate( | |
| self.predicate, dict(assignments), list(read_columns or ()))) | |
| def delete(self): | |
| return from_native_commit_messages( | |
| self.table, self.writer.delete_by_predicate(self.predicate)) |