zhuxiangyi opened a new pull request, #9356:
URL: https://github.com/apache/paimon/pull/9356

   ### Purpose
   
   `MERGE INTO` with a `DELETE` clause silently corrupts a `partial-update` 
primary key table: the
   statement succeeds and commits a snapshot, but the table becomes unreadable 
afterwards.
   
   ```sql
   CREATE TABLE t (id INT, a INT, b INT) TBLPROPERTIES (
     'primary-key' = 'id', 'bucket' = '1', 'merge-engine' = 'partial-update');
   INSERT INTO t VALUES (1, 10, 100), (2, 20, 200);
   
   MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE;  -- succeeds, 
commits a snapshot
   SELECT * FROM t;                                               -- throws
   ```
   
   ```
   java.lang.IllegalArgumentException: By default, Partial update can not 
accept delete records,
   you can choose one of the following solutions:
   1. Configure 'ignore-delete' to ignore delete records.
   2. Configure 'partial-update.remove-record-on-delete' to remove the whole 
row ...
     at 
org.apache.paimon.mergetree.compact.PartialUpdateMergeFunction.add(PartialUpdateMergeFunction.java:191)
   ```
   
   The write side never notices; the failure only surfaces at read time, and 
only for queries that
   actually have to merge the affected key — so the table looks partly healthy, 
which makes this hard
   to diagnose.
   
   **Root cause.** `PrimaryKeyTableUtils#validatePKUpsertDeletable` already 
encodes which merge engines
   can consume delete records. It is called by Flink's SQL `DELETE`
   (`SupportsRowLevelOperationFlinkTableSink#applyRowLevelDelete` / 
`#applyDeleteFilters`) and by
   Spark's `DeleteFromPaimonTableCommand`, but **not** by Spark's `MERGE INTO`, 
which emits
   `RowKind.DELETE` records unconditionally. `RowLevelOp.MergeInto` only checks 
that the merge engine
   is `deduplicate` or `partial-update`, and `partial-update` is exactly the 
one that cannot accept
   delete records by default.
   
   **Fix.** Run the same validation in `PaimonMergeInto` when the statement has 
a `DELETE` clause and
   the target is a primary key table, so all three paths reject the same tables 
with the same message.
   
   The check lives in the analysis rule rather than in the command because both 
V2 row-level paths
   (`SparkTable#supportsV2CopyOnWriteOps` / `#supportsV2DeltaOps`) require 
`primaryKeys().isEmpty` —
   primary key tables therefore always go through `PaimonMergeInto` on every 
supported Spark version,
   making it the single choke point. Failing during analysis also means nothing 
has been written yet.
   
   **Behavior change.** Such statements now fail with the existing, actionable 
message instead of
   committing a corrupt snapshot:
   
   > Merge engine partial-update doesn't support batch delete by default. To 
support batch delete,
   > please set partial-update.remove-record-on-delete to true when there is no 
sequence.field or set
   > partial-update.remove-record-on-sequence-group.
   
   Scope is narrow — only *primary key table* + *statement contains a DELETE 
clause* + *merge engine
   cannot accept delete records*:
   
   - `aggregate` / `first-row` are already rejected earlier by 
`RowLevelOp.MergeInto`.
   - `deduplicate` passes the validation.
   - `partial-update` passes once `partial-update.remove-record-on-delete` or
     `partial-update.remove-record-on-sequence-group` is set.
   - Merges without a `DELETE` clause, and append-only tables, are unaffected.
   
   This also stops the related silent no-op: with `ignore-delete = true`, a 
`MERGE ... THEN DELETE`
   used to report success while keeping the row, because 
`PartialUpdateMergeFunction#add` discards the
   delete record at read time. It is now rejected instead.
   
   ### Tests
   
   New in `MergeIntoPrimaryKeyTableTest` (`MergeIntoTableTestBase.scala`):
   
   - `reject DELETE clause when the merge engine can not accept deletes` — 
covers
     `WHEN MATCHED THEN DELETE`, `WHEN MATCHED AND <cond> THEN DELETE` and
     `WHEN NOT MATCHED BY SOURCE THEN DELETE`, and asserts the target is left 
untouched.
   - `allow DELETE clause once the merge engine can accept deletes` — 
`partial-update` with
     `partial-update.remove-record-on-delete = true` still deletes.
   - `a delete-free merge on partial-update is unaffected`.
   
   Existing suites, all green with no changes:
   
   - `MergeInto{PrimaryKeyBucketed,PrimaryKeyNonBucket}TableTest` and their 
`V2` variants —
     `Tests: succeeded 244, failed 0`
   - `MergeIntoAppend{Bucketed,NonBucketed}TableTest` + `V2` variants, 
`DeleteFromTableTest` /
     `V2DeleteFromTableTest`, `UpdateTableTest` / `V2UpdateTableTest` —
     `Tests: succeeded 352, failed 0`
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to