This is an automated email from the ASF dual-hosted git repository.

airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 2fb774e8cd1 [fix](fe) Check conflicting index change jobs before 
dropping index metadata (#68674)
2fb774e8cd1 is described below

commit 2fb774e8cd11e737a328cf8f3656453b0259dd82
Author: Jack <[email protected]>
AuthorDate: Thu Oct 22 10:54:55 2026 +0800

    [fix](fe) Check conflicting index change jobs before dropping index 
metadata (#68674)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    On the light index change path, `DROP INDEX` can fail with `partition
    xxx has been built specified index` when an unfinished IndexChangeJob
    conflicts on the same partition. Although the statement returns an
    error, the index metadata has already been removed and `SHOW INDEX` no
    longer lists it, so the client cannot tell whether the DDL took effect.
    
    Reproduce:
    - Cloud mode (any unfinished index change job on the partition
    conflicts): create a table, then quickly run `CREATE INDEX iv ... USING
    INVERTED; DROP INDEX iv; CREATE INDEX ng ... USING NGRAM_BF; DROP INDEX
    ng;`. The last `DROP INDEX` fails, yet `SHOW INDEX` is empty afterwards.
    - Local mode (only a job for the same index and direction conflicts):
    `ALTER TABLE t DROP INDEX idx PARTITION (p1)` and, while that job is
    unfinished, `ALTER TABLE t DROP INDEX idx`.
    
    Root cause: in `SchemaChangeHandler#modifyTableLightSchemaChange`, a
    light `DROP INDEX` first calls `updateBaseIndexSchema` and writes the
    edit log, and only afterwards submits its delete jobs in
    `buildOrDeleteTableInvertedIndices`, which runs the conflict check
    (`hasIndexChangeJobOnPartition`) and, in cloud mode, the compute group
    lookup (`getCloudClusterName`). When either throws, the metadata change
    is not rolled back.
    
    Fix: `SchemaChangeHandler#process` runs both checks for every full-table
    `DROP INDEX` that takes the light index change path before any ALTER
    clause is processed, so a rejected statement leaves the table unchanged.
    They have to run before the clause loop rather than just before
    `updateBaseIndexSchema`, because clauses such as `DROP COLUMN` update
    table metadata in place (the bloom filter columns) while they are
    processed. The light-drop eligibility rule is extracted into
    `isLightDropIndexSupported` and shared by the pre-check and the clause
    loop. The conflict check reuses `hasIndexChangeJobOnPartition` with the
    existing error message; the later check in
    `buildOrDeleteTableInvertedIndices` is unchanged.
    
    The same pre-check also rejects a light `ADD INDEX` and a light
    full-table `DROP INDEX` in one statement (`Can not add and drop indexes
    in one ALTER statement, please run them in separate statements.`). The
    light path builds the delete jobs of all altered indexes with one
    direction flag decided by the last index clause, so `DROP INDEX old, ADD
    INDEX new` left the files of `old` behind and `ADD INDEX new, DROP INDEX
    old` also deleted the files of `new`. Only the case where both clauses
    take the light path is rejected; a statement whose ADD or DROP is not
    light keeps its behavior (e.g. replacing NGRAM_BF with BLOOMFILTER in
    local mode).
---
 .../apache/doris/alter/SchemaChangeHandler.java    | 78 +++++++++++++++----
 .../doris/alter/SchemaChangeHandlerTest.java       | 90 ++++++++++++++++++++++
 2 files changed, 153 insertions(+), 15 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java 
b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
index 5afc1db6b30..5d8f17fbef4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
@@ -2388,6 +2388,11 @@ public class SchemaChangeHandler extends AlterHandler {
                 }
             }
 
+            // A light DROP INDEX publishes the index metadata before it 
submits its delete jobs. Run the checks
+            // that can reject those jobs before any clause below touches 
table metadata (processDropColumn
+            // updates the bloom filter columns in place), so a rejected 
statement leaves the table unchanged.
+            checkLightDropIndexPreconditions(db, olapTable, alterOps, 
enableAddIndexForNewData);
+
             for (AlterOp alterOp : alterOps) {
                 Map<String, String> properties = alterOp.getProperties();
                 // alter table properties
@@ -2641,21 +2646,7 @@ public class SchemaChangeHandler extends AlterHandler {
                                 break;
                             }
                         }
-                        // Inverted index supports light schema change in both 
cloud and local mode.
-                        // NGRAM_BF and BfIndex support it only in cloud mode.
-                        boolean supportLightIndexChange = false;
-                        if (found != null) {
-                            if (Config.isCloudMode()) {
-                                supportLightIndexChange = 
enableAddIndexForNewData
-                                        && (found.getIndexType() == 
IndexType.NGRAM_BF
-                                        || found.getIndexType() == 
IndexType.BLOOMFILTER
-                                        || found.getIndexType() == 
IndexType.INVERTED);
-                            } else {
-                                supportLightIndexChange = found.getIndexType() 
== IndexType.INVERTED
-                                        || found.getIndexType() == 
IndexType.ANN;
-                            }
-                        }
-                        if (supportLightIndexChange) {
+                        if (found != null && isLightDropIndexSupported(found, 
enableAddIndexForNewData)) {
                             alterIndexes.add(found);
                             isDropIndex = true;
                             lightIndexChange = true;
@@ -3921,6 +3912,63 @@ public class SchemaChangeHandler extends AlterHandler {
         }
     }
 
+    // Inverted index supports light schema change in both cloud and local 
mode.
+    // NGRAM_BF and BfIndex support it only in cloud mode.
+    private static boolean isLightDropIndexSupported(Index index, boolean 
enableAddIndexForNewData) {
+        if (Config.isCloudMode()) {
+            return enableAddIndexForNewData
+                    && (index.getIndexType() == IndexType.NGRAM_BF
+                    || index.getIndexType() == IndexType.BLOOMFILTER
+                    || index.getIndexType() == IndexType.INVERTED);
+        }
+        return index.getIndexType() == IndexType.INVERTED || 
index.getIndexType() == IndexType.ANN;
+    }
+
+    // Throws if a full-table DROP INDEX in alterOps takes the light index 
change path and would then fail to
+    // submit its delete jobs: no compute group, or an unfinished index change 
job on the dropped index.
+    // Also rejects a light DROP INDEX together with a light ADD INDEX: their 
delete jobs carry one direction
+    // for all altered indexes, so the added index would be deleted or the 
dropped one would be kept.
+    private void checkLightDropIndexPreconditions(Database db, OlapTable 
olapTable, List<AlterOp> alterOps,
+            boolean enableAddIndexForNewData) throws UserException {
+        if (!Config.enable_light_index_change) {
+            return;
+        }
+        List<Index> droppedIndexes = new ArrayList<>();
+        for (AlterOp alterOp : alterOps) {
+            if (!(alterOp instanceof DropIndexOp) || ((DropIndexOp) 
alterOp).hasPartitionSpec()) {
+                continue;
+            }
+            String indexName = ((DropIndexOp) alterOp).getIndexName();
+            olapTable.getIndexes().stream()
+                    .filter(index -> 
index.getIndexName().equalsIgnoreCase(indexName))
+                    .filter(index -> isLightDropIndexSupported(index, 
enableAddIndexForNewData))
+                    .findFirst()
+                    .ifPresent(droppedIndexes::add);
+        }
+        if (droppedIndexes.isEmpty()) {
+            return;
+        }
+        if (alterOps.stream().anyMatch(alterOp -> alterOp instanceof 
CreateIndexOp
+                && ((CreateIndexOp) 
alterOp).getIndex().isLightAddIndexSupported(enableAddIndexForNewData))) {
+            throw new DdlException("Can not add and drop indexes in one ALTER 
statement,"
+                    + " please run them in separate statements.");
+        }
+        getCloudClusterName();
+        checkNoIndexChangeJobOnPartitions(db, olapTable, droppedIndexes, 
olapTable.getPartitionNames());
+    }
+
+    // Throws if any of the partitions still has an unfinished index change 
job on the base index.
+    private void checkNoIndexChangeJobOnPartitions(Database db, OlapTable 
olapTable, List<Index> alterIndexes,
+            Set<String> partitionNames) throws DdlException {
+        for (String partitionName : partitionNames) {
+            if (hasIndexChangeJobOnPartition(olapTable.getBaseIndexId(), 
db.getId(), olapTable.getId(),
+                    partitionName, alterIndexes, true)) {
+                throw new DdlException("partition " + partitionName + " has 
been built specified index."
+                        + " please check your build stmt.");
+            }
+        }
+    }
+
     public boolean hasIndexChangeJobOnPartition(
             long originIndexId, long dbId, long tableId, String partitionName,
             List<Index> alterIndexes, boolean isDrop) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/alter/SchemaChangeHandlerTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/alter/SchemaChangeHandlerTest.java
index 6ea6512b0db..1189f5a5903 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/alter/SchemaChangeHandlerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/alter/SchemaChangeHandlerTest.java
@@ -1293,6 +1293,96 @@ public class SchemaChangeHandlerTest extends 
TestWithFeService {
         }
     }
 
+    @Test
+    public void testDropIndexRejectedByIndexChangeJobKeepsMetadata() throws 
Exception {
+        String tableName = "sc_drop_index_conflict";
+        dropTable("test." + tableName, false);
+        createTable("CREATE TABLE test." + tableName + " (\n"
+                + "k1 INT,\n"
+                + "v1 STRING,\n"
+                + "v2 STRING,\n"
+                + "INDEX idx_v1 (v1) USING INVERTED\n"
+                + ") ENGINE=OLAP\n"
+                + "DUPLICATE KEY(k1)\n"
+                + "DISTRIBUTED BY HASH(k1) BUCKETS 1\n"
+                + "PROPERTIES ('replication_num' = '1', 'light_schema_change' 
= 'true', "
+                + "'bloom_filter_columns' = 'v2');");
+
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        OlapTable tbl = (OlapTable) db.getTableOrMetaException(tableName, 
Table.TableType.OLAP);
+        SchemaChangeHandler handler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        Index index = tbl.getIndexes().stream()
+                .filter(idx -> idx.getIndexName().equals("idx_v1"))
+                .findFirst().get();
+        int schemaVersion = 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion();
+
+        // An unfinished drop job on the same index, e.g. left by DROP INDEX 
... PARTITION.
+        // It is only registered in indexChangeJobs so that the job scheduler 
does not run or cancel it.
+        long jobId = Env.getCurrentEnv().getNextId();
+        IndexChangeJob job = new IndexChangeJob(jobId, db.getId(), 
tbl.getId(), tbl.getName(), 1000L, 0,
+                Lists.newArrayList(), Lists.newArrayList());
+        job.setOriginIndexId(tbl.getBaseIndexId());
+        job.setPartitionName(tbl.getPartitionNames().iterator().next());
+        job.setAlterInvertedIndexInfo(true, Lists.newArrayList(index));
+        handler.getIndexChangeJobs().put(jobId, job);
+        try {
+            String dropIndexStmt = "ALTER TABLE test." + tableName + " DROP 
INDEX idx_v1";
+            Exception e = Assertions.assertThrows(Exception.class, () -> 
alterTable(dropIndexStmt, connectContext));
+            Assertions.assertTrue(e.getMessage().contains("has been built 
specified index"), e.getMessage());
+            Assertions.assertTrue(tbl.getIndexes().stream().anyMatch(idx -> 
idx.getIndexName().equals("idx_v1")));
+            Assertions.assertEquals(schemaVersion, 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion());
+
+            // The check must run before DROP COLUMN updates the bloom filter 
columns in place.
+            String dropColumnAndIndexStmt = "ALTER TABLE test." + tableName + 
" DROP COLUMN v2, DROP INDEX idx_v1";
+            e = Assertions.assertThrows(Exception.class, () -> 
alterTable(dropColumnAndIndexStmt, connectContext));
+            Assertions.assertTrue(e.getMessage().contains("has been built 
specified index"), e.getMessage());
+            Assertions.assertEquals(Sets.newHashSet("v2"), 
tbl.getCopiedBfColumns());
+            Assertions.assertNotNull(tbl.getColumn("v2"));
+
+            job.setJobState(IndexChangeJob.JobState.FINISHED);
+            alterTable(dropIndexStmt, connectContext);
+            jobSize++;
+            Assertions.assertTrue(tbl.getIndexes().stream().noneMatch(idx -> 
idx.getIndexName().equals("idx_v1")));
+        } finally {
+            handler.getIndexChangeJobs().remove(jobId);
+        }
+    }
+
+    @Test
+    public void testLightAddAndDropIndexInOneStatementRejected() throws 
Exception {
+        String tableName = "sc_light_add_and_drop_index";
+        dropTable("test." + tableName, false);
+        createTable("CREATE TABLE test." + tableName + " (\n"
+                + "k1 INT,\n"
+                + "v1 STRING,\n"
+                + "v2 STRING,\n"
+                + "INDEX idx_v1 (v1) USING INVERTED\n"
+                + ") ENGINE=OLAP\n"
+                + "DUPLICATE KEY(k1)\n"
+                + "DISTRIBUTED BY HASH(k1) BUCKETS 1\n"
+                + "PROPERTIES ('replication_num' = '1', 'light_schema_change' 
= 'true');");
+
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        OlapTable tbl = (OlapTable) db.getTableOrMetaException(tableName, 
Table.TableType.OLAP);
+        SchemaChangeHandler handler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        int schemaVersion = 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion();
+        int indexChangeJobCount = handler.getIndexChangeJobs().size();
+
+        // Both clauses take the light index change path, whose delete jobs 
carry a single direction.
+        for (String clauses : Lists.newArrayList(
+                "DROP INDEX idx_v1, ADD INDEX idx_v2 (v2) USING INVERTED",
+                "ADD INDEX idx_v2 (v2) USING INVERTED, DROP INDEX idx_v1")) {
+            String sql = "ALTER TABLE test." + tableName + " " + clauses;
+            Exception e = Assertions.assertThrows(Exception.class, () -> 
alterTable(sql, connectContext));
+            Assertions.assertTrue(e.getMessage().contains("Can not add and 
drop indexes in one ALTER statement"),
+                    e.getMessage());
+        }
+        Assertions.assertEquals(Lists.newArrayList("idx_v1"),
+                
tbl.getIndexes().stream().map(Index::getIndexName).collect(Collectors.toList()));
+        Assertions.assertEquals(schemaVersion, 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion());
+        Assertions.assertEquals(indexChangeJobCount, 
handler.getIndexChangeJobs().size());
+    }
+
     @Test
     public void testAddInvertedIndexStoresCanonicalBuiltinAnalyzer() throws 
Exception {
         createAnalyzerAliasTable("sc_ik_alias");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to