pingcap / pingcap/tidb

EXCHANGE PARTITION can orphan masking policies after table ID swap

Open
#69,754 0 comments 0 reactions 0 assignees View on GitHub
affects-9.0 component/ddl found-by-ai may-affects-7.5 may-affects-8.1 may-affects-8.5 severity/major sig/sql-infra type/bug
Dominant language
Go
Stars
40.5k
Forks
6.2k
PR merge metrics
PR metrics pending

Description

## Bug Report

Please answer these questions before submitting your issue. Thanks!

### 1. Minimal reproduce step (Required)

A masking policy created on a standalone table can become unreachable after the table is exchanged with a partition. The policy row is still present in `mysql.tidb_masking_policy`, but normal table DDL can no longer disable or drop it by the logical table name.

```sql
DROP DATABASE IF EXISTS exchange_masking_repro;
CREATE DATABASE exchange_masking_repro;
USE exchange_masking_repro;

CREATE TABLE pt(a INT, KEY idx_a(a))
PARTITION BY RANGE(a) (
PARTITION p0 VALUES LESS THAN (10),
PARTITION p1 VALUES LESS THAN (100)
);
CREATE TABLE nt(a INT, KEY idx_a(a));
INSERT INTO pt VALUES (1),(20);
INSERT INTO nt VALUES (5);

CREATE MASKING POLICY mp_nt ON nt(a) AS a ENABLE;

SELECT policy_name, db_name, table_name, table_id, column_name, column_id, status
FROM mysql.tidb_masking_policy
WHERE db_name = DATABASE();

-- This works before EXCHANGE PARTITION.
ALTER TABLE nt DISABLE MASKING POLICY mp_nt;
ALTER TABLE nt ENABLE MASKING POLICY mp_nt;

ALTER TABLE pt EXCHANGE PARTITION p0 WITH TABLE nt;

SELECT table_name, tidb_table_id
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name IN ('pt','nt')
ORDER BY table_name;

SELECT table_name, partition_name, tidb_partition_id
FROM information_schema.partitions
WHERE table_schema = DATABASE()
AND table_name = 'pt'
ORDER BY partition_name;

SELECT policy_name, db_name, table_name, table_id, column_name, column_id, status
FROM mysql.tidb_masking_policy
WHERE db_name = DATABASE();

ALTER TABLE nt DISABLE MASKING POLICY mp_nt;
ALTER TABLE nt DROP MASKING POLICY mp_nt;
```

The key observation is that after `EXCHANGE PARTITION`, the current table ID of `nt` changes, but the masking policy row keeps the old physical ID, which is now the partition ID of `pt.p0`.

### 2. What did you expect to see? (Required)

`EXCHANGE PARTITION` should not leave masking-policy metadata attached to a stale physical ID while the logical table name still points to the exchanged standalone table.

A safe behavior would be either:

* reject `EXCHANGE PARTITION` when the standalone table or exchanged partition owns masking policies; or
* atomically remap the relevant `mysql.tidb_masking_policy` rows so that `DISABLE`, `ENABLE`, `DROP`, and re-create all operate on exactly one live logical owner after the exchange.

### 3. What did you see instead (Required)

The exchange succeeds, but the policy becomes unreachable through ordinary DDL:

```text
ALTER TABLE nt DISABLE MASKING POLICY mp_nt;
ERROR 1105 (HY000): masking policy mp_nt doesn't exist

ALTER TABLE nt DROP MASKING POLICY mp_nt;
ERROR 1105 (HY000): masking policy mp_nt doesn't exist
```

Trying the partitioned table name also does not reach the orphaned policy:

```text
ALTER TABLE pt DISABLE MASKING POLICY mp_nt;
ERROR 1105 (HY000): masking policy mp_nt doesn't exist
```

The stale row remains visible in `mysql.tidb_masking_policy`. Recreating a policy with the same name on `nt` can create another row on the new table ID, and later management DDL affects only the new row, leaving the old row orphaned.

### 4. What is your TiDB version? (Required)

```text
Release Version: v9.0.0-beta.2.pre-1774-g81ec977cb8
Git Commit Hash: 81ec977cb8bf97e0c9805dfc0be8ffcbd4b0bbeb
UTC Build Time: 2026-05-28 03:35:30
Edition: Community
Store: tikv
```

Likely root cause

`EXCHANGE PARTITION` swaps the standalone table ID with the partition ID. Several DDL paths already repair masking-policy side metadata when they change logical ownership, for example truncate remaps masking-policy table IDs and rename updates policy names. The exchange-partition path performs the physical ID swap but does not appear to perform an equivalent masking-policy remap or compatibility check. After the swap, masking-policy lookup uses the current logical table ID and therefore cannot find the stale row that still records the old physical ID.

Contributor guide

Open the contributing guide

Research direction

Reproduce the SQL sequence and inspect the EXCHANGE PARTITION DDL path, then compare its masking-policy metadata handling with the truncate and rename paths mentioned in the report. Done means the exchange either rejects affected policies or keeps policy ownership consistent so disable, enable, drop, and recreate operate on one live row.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.