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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 3f4002e778b branch-4.1: [feature](index) Support partition DROP INDEX 
with conflict guards (#70357)
3f4002e778b is described below

commit 3f4002e778bd4bdac135b349f349e5ed4d7a5093
Author: Jack <[email protected]>
AuthorDate: Fri Nov 6 21:52:55 2026 +0800

    branch-4.1: [feature](index) Support partition DROP INDEX with conflict 
guards (#70357)
    
    ### What problem does this PR solve?
    
    Issue Number: No separate issue is linked by the source PRs.
    
    Related PRs: [#62181](https://github.com/apache/doris/pull/62181),
    [#69442](https://github.com/apache/doris/pull/69442),
    [#69443](https://github.com/apache/doris/pull/69443)
    
    Backport #62181 with the two follow-up correctness fixes #69442 and
    #69443. Partition DROP INDEX deletes physical inverted-index files from
    selected partitions while retaining the table index definition. Mixed
    ALTER work and unfinished BUILD/DROP jobs on the same index are rejected
    before metadata changes. The older target needs a DropIndexClause
    adapter and a minimal early full-table conflict check.
---
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |   5 +-
 .../org/apache/doris/alter/IndexChangeJob.java     |  14 +-
 .../apache/doris/alter/SchemaChangeHandler.java    | 169 ++++++++++---
 .../org/apache/doris/analysis/DropIndexClause.java |  36 +++
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  16 +-
 .../trees/plans/commands/info/DropIndexOp.java     |  44 +++-
 .../org/apache/doris/alter/IndexChangeJobTest.java | 131 ++++++++++
 .../doris/alter/SchemaChangeHandlerTest.java       | 178 ++++++++++++++
 .../test_drop_index_on_partition.groovy            | 269 +++++++++++++++++++++
 9 files changed, 813 insertions(+), 49 deletions(-)

diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index 5d9325534f7..16cb8d92e00 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -363,7 +363,8 @@ supportedDropStatement
     | DROP (DATABASE | SCHEMA) (IF EXISTS)? name=multipartIdentifier FORCE?    
 #dropDatabase
     | DROP statementScope? FUNCTION (IF EXISTS)?
         functionIdentifier LEFT_PAREN functionArguments? RIGHT_PAREN           
 #dropFunction
-    | DROP INDEX (IF EXISTS)? name=identifier ON tableName=multipartIdentifier 
 #dropIndex
+    | DROP INDEX (IF EXISTS)? name=identifier ON tableName=multipartIdentifier
+        partitionSpec?                                                         
   #dropIndex
     | DROP RESOURCE (IF EXISTS)? name=identifierOrText                         
 #dropResource
     | DROP ROW POLICY (IF EXISTS)? policyName=identifier
         ON tableName=multipartIdentifier
@@ -793,7 +794,7 @@ alterTableClause
     | RENAME PARTITION name=identifier newName=identifier                      
     #renamePartitionClause
     | RENAME COLUMN name=qualifiedName TO? newName=identifier                  
     #renameColumnClause
     | ADD indexDef                                                             
     #addIndexClause
-    | DROP INDEX (IF EXISTS)? name=identifier                                  
     #dropIndexClause
+    | DROP INDEX (IF EXISTS)? name=identifier partitionSpec?                   
       #dropIndexClause
     | ENABLE FEATURE name=STRING_LITERAL (WITH properties=propertyClause)?     
     #enableFeatureClause
     | MODIFY DISTRIBUTION (DISTRIBUTED BY (HASH hashKeys=identifierList | 
RANDOM)
         (BUCKETS (INTEGER_VALUE | autoBucket=AUTO))?)?                         
     #modifyDistributionClause
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/alter/IndexChangeJob.java 
b/fe/fe-core/src/main/java/org/apache/doris/alter/IndexChangeJob.java
index f25f82cda90..facb618b372 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/IndexChangeJob.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/IndexChangeJob.java
@@ -173,13 +173,13 @@ public class IndexChangeJob implements Writable {
         this.alterInvertedIndexes = alterInvertedIndexes;
     }
 
-    public boolean hasSameAlterInvertedIndex(boolean isDropOp, List<Index> 
inputAlterInvertedIndexes) {
-        if (this.isDropOp == isDropOp) {
-            for (Index inputIndex : inputAlterInvertedIndexes) {
-                for (Index existIndex : this.alterInvertedIndexes) {
-                    if (inputIndex.getIndexId() == existIndex.getIndexId()) {
-                        return true;
-                    }
+    // Whether this job builds or drops any of the given indexes, in either 
direction: jobs of one index are
+    // not ordered, so a build running after a drop would add the dropped 
index back, and the other way round.
+    public boolean hasAlterInvertedIndex(List<Index> 
inputAlterInvertedIndexes) {
+        for (Index inputIndex : inputAlterInvertedIndexes) {
+            for (Index existIndex : this.alterInvertedIndexes) {
+                if (inputIndex.getIndexId() == existIndex.getIndexId()) {
+                    return true;
                 }
             }
         }
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 921da654421..4d238e871c9 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
@@ -1999,6 +1999,15 @@ public class SchemaChangeHandler extends AlterHandler {
             Map<Long, Set<String>> indexOnPartitions = new HashMap<>();
             boolean isDropIndex = false;
             Map<String, String> propertyMap = new HashMap<>();
+            checkLightDropIndexJobConflicts(db, olapTable, alterClauses, 
enableAddIndexForNewData);
+            // Partition index jobs cannot apply other schema changes or 
independent partition scopes.
+            if (alterClauses.size() > 1 && alterClauses.stream().anyMatch(
+                    alterClause -> alterClause instanceof DropIndexClause
+                            && ((DropIndexClause) 
alterClause).hasPartitionSpec())) {
+                throw new DdlException("DROP INDEX on partitions can not be 
combined with other clauses in one"
+                        + " ALTER statement, please run them in separate 
statements.");
+            }
+
             for (AlterClause alterClause : alterClauses) {
                 Map<String, String> properties = alterClause.getProperties();
                 // alter table properties
@@ -2179,37 +2188,72 @@ public class SchemaChangeHandler extends AlterHandler {
                     buildIndexChange = true;
                     lightSchemaChange = false;
                 } else if (alterClause instanceof DropIndexClause) {
-                    if (processDropIndex((DropIndexClause) alterClause, 
olapTable, newIndexes)) {
-                        return;
-                    }
-                    lightSchemaChange = false;
-
                     DropIndexClause dropIndexClause = (DropIndexClause) 
alterClause;
-                    List<Index> existedIndexes = olapTable.getIndexes();
-                    Index found = null;
-                    for (Index existedIdx : existedIndexes) {
-                        if 
(existedIdx.getIndexName().equalsIgnoreCase(dropIndexClause.getIndexName())) {
-                            found = existedIdx;
-                            break;
+
+                    if (dropIndexClause.hasPartitionSpec()) {
+                        // Delete the physical index files and keep the 
table-level index definition.
+                        List<Index> existedIndexes = olapTable.getIndexes();
+                        Index found = null;
+                        for (Index existedIdx : existedIndexes) {
+                            if 
(existedIdx.getIndexName().equalsIgnoreCase(dropIndexClause.getIndexName())) {
+                                found = existedIdx;
+                                break;
+                            }
                         }
-                    }
-                    // for inverted index, light schema change is supported in 
both cloud and local mode;
-                    // for ngram index, light schema change is supported only 
in cloud mode;
-                    boolean supportLightIndexChange = false;
-                    if (Config.isCloudMode()) {
-                        if (enableAddIndexForNewData) {
-                            supportLightIndexChange = (
-                                    found.getIndexType() == IndexType.NGRAM_BF
-                                            || found.getIndexType() == 
IndexDef.IndexType.INVERTED);
+                        if (found == null) {
+                            if (dropIndexClause.isSetIfExists()) {
+                                LOG.info("drop index[{}] which does not exist 
on table[{}]",
+                                        dropIndexClause.getIndexName(), 
olapTable.getName());
+                                return;
+                            }
+                            throw new DdlException("index " + 
dropIndexClause.getIndexName() + " does not exist");
                         }
-                    } else {
-                        supportLightIndexChange = found.getIndexType() == 
IndexDef.IndexType.INVERTED
-                                || found.getIndexType() == 
IndexDef.IndexType.ANN;
-                    }
-                    if (found != null && supportLightIndexChange) {
+                        if (found.getIndexType() != IndexType.INVERTED) {
+                            throw new DdlException(
+                                    "Only inverted index supports DROP INDEX 
ON PARTITION");
+                        }
+                        if (!olapTable.isPartitionedTable()) {
+                            throw new DdlException("table " + 
olapTable.getName()
+                                    + " is not partitioned, cannot drop index 
with partitions");
+                        }
+                        Set<String> specifiedPartitions = new 
HashSet<>(dropIndexClause.getPartitionNames());
+                        for (String partName : specifiedPartitions) {
+                            if (olapTable.getPartition(partName) == null) {
+                                throw new DdlException("partition " + partName 
+ " does not exist");
+                            }
+                        }
+
+                        if (!Config.enable_light_index_change) {
+                            throw new DdlException(
+                                    "DROP INDEX ON PARTITION requires 
enable_light_index_change=true");
+                        }
+                        checkNoIndexChangeJobOnPartitions(db, olapTable,
+                                ImmutableList.of(found), specifiedPartitions);
+
                         alterIndexes.add(found);
+                        indexOnPartitions.put(found.getIndexId(), 
specifiedPartitions);
                         isDropIndex = true;
-                        lightIndexChange = true;
+                        buildIndexChange = true;
+                        lightSchemaChange = false;
+                    } else {
+                        if (processDropIndex(dropIndexClause, olapTable, 
newIndexes)) {
+                            return;
+                        }
+                        lightSchemaChange = false;
+
+                        List<Index> existedIndexes = olapTable.getIndexes();
+                        Index found = null;
+                        for (Index existedIdx : existedIndexes) {
+                            if 
(existedIdx.getIndexName().equalsIgnoreCase(dropIndexClause.getIndexName())) {
+                                found = existedIdx;
+                                break;
+                            }
+                        }
+                        if (found != null && isLightDropIndexSupported(found, 
enableAddIndexForNewData)) {
+                            alterIndexes.add(found);
+                            isDropIndex = true;
+                            lightIndexChange = true;
+                        }
                     }
                 } else {
                     Preconditions.checkState(false);
@@ -2239,7 +2283,7 @@ public class SchemaChangeHandler extends AlterHandler {
                 }
                 if (Config.enable_light_index_change) {
                     buildOrDeleteTableInvertedIndices(db, olapTable, 
indexSchemaMap,
-                            alterIndexes, indexOnPartitions, false);
+                            alterIndexes, indexOnPartitions, isDropIndex);
                 }
             } else {
                 createJob(rawSql, db.getId(), olapTable, indexSchemaMap, 
propertyMap, newIndexes);
@@ -3411,7 +3455,7 @@ public class SchemaChangeHandler extends AlterHandler {
                     }
 
                     if (hasIndexChangeJobOnPartition(originIndexId, 
db.getId(), olapTable.getId(),
-                            partitionName, alterIndexes, isDropOp)) {
+                            partitionName, alterIndexes)) {
                         throw new DdlException("partition " + partitionName + 
" has been built specified index."
                                             + " please check your build 
stmt.");
                     }
@@ -3435,25 +3479,76 @@ public class SchemaChangeHandler extends AlterHandler {
         }
     }
 
+    private static boolean isLightDropIndexSupported(Index index, boolean 
enableAddIndexForNewData) {
+        if (Config.isCloudMode()) {
+            return enableAddIndexForNewData
+                    && (index.getIndexType() == IndexType.NGRAM_BF
+                    || index.getIndexType() == IndexType.INVERTED);
+        }
+        return index.getIndexType() == IndexType.INVERTED || 
index.getIndexType() == IndexType.ANN;
+    }
+
+    // Reject conflicting jobs before a light full-table drop changes the 
index metadata.
+    private void checkLightDropIndexJobConflicts(Database db, OlapTable 
olapTable, List<AlterClause> alterClauses,
+            boolean enableAddIndexForNewData) throws DdlException {
+        if (!Config.enable_light_index_change) {
+            return;
+        }
+        List<Index> droppedIndexes = new ArrayList<>();
+        for (AlterClause alterClause : alterClauses) {
+            if (!(alterClause instanceof DropIndexClause)
+                    || ((DropIndexClause) alterClause).hasPartitionSpec()) {
+                continue;
+            }
+            String indexName = ((DropIndexClause) alterClause).getIndexName();
+            olapTable.getIndexes().stream()
+                    .filter(index -> 
index.getIndexName().equalsIgnoreCase(indexName))
+                    .filter(index -> isLightDropIndexSupported(index, 
enableAddIndexForNewData))
+                    .findFirst()
+                    .ifPresent(droppedIndexes::add);
+        }
+        if (droppedIndexes.isEmpty()) {
+            return;
+        }
+        checkNoIndexChangeJobOnPartitions(db, olapTable, droppedIndexes, 
olapTable.getPartitionNames());
+    }
+
+    private void checkNoIndexChangeJobOnPartitions(Database db, OlapTable 
olapTable, List<Index> alterIndexes,
+            Set<String> partitionNames) throws DdlException {
+        for (IndexChangeJob indexChangeJob : 
ImmutableList.copyOf(indexChangeJobs.values())) {
+            if (partitionNames.contains(indexChangeJob.getPartitionName())
+                    && isConflictingIndexChangeJob(indexChangeJob, 
olapTable.getBaseIndexId(), db.getId(),
+                    olapTable.getId(), alterIndexes)) {
+                throw new DdlException("partition " + 
indexChangeJob.getPartitionName() + " of table "
+                        + olapTable.getName() + " has an unfinished index 
change job, please retry after it finishes.");
+            }
+        }
+    }
+
     public boolean hasIndexChangeJobOnPartition(
             long originIndexId, long dbId, long tableId, String partitionName,
-            List<Index> alterIndexes, boolean isDrop) {
+            List<Index> alterIndexes) {
         // TODO: this is temporary methods
         for (IndexChangeJob indexChangeJob : 
ImmutableList.copyOf(indexChangeJobs.values())) {
-            if (indexChangeJob.getOriginIndexId() == originIndexId
-                    && indexChangeJob.getDbId() == dbId
-                    && indexChangeJob.getTableId() == tableId
-                    && indexChangeJob.getPartitionName().equals(partitionName)
-                    && ((Config.isNotCloudMode() && 
indexChangeJob.hasSameAlterInvertedIndex(isDrop, alterIndexes))
-                    || Config.isCloudMode())
-                    && !indexChangeJob.isDone()) {
-                // if JobState is done (CANCELLED or FINISHED), also allow 
user to create job again
+            if (indexChangeJob.getPartitionName().equals(partitionName)
+                    && isConflictingIndexChangeJob(indexChangeJob, 
originIndexId, dbId, tableId, alterIndexes)) {
                 return true;
             }
         }
         return false;
     }
 
+    private static boolean isConflictingIndexChangeJob(IndexChangeJob 
indexChangeJob, long originIndexId,
+            long dbId, long tableId, List<Index> alterIndexes) {
+        // if JobState is done (CANCELLED or FINISHED), also allow user to 
create job again
+        return indexChangeJob.getOriginIndexId() == originIndexId
+                && indexChangeJob.getDbId() == dbId
+                && indexChangeJob.getTableId() == tableId
+                && ((Config.isNotCloudMode() && 
indexChangeJob.hasAlterInvertedIndex(alterIndexes))
+                || Config.isCloudMode())
+                && !indexChangeJob.isDone();
+    }
+
     // schema change job is not allowed when index change job is running
     boolean hasIndexChangeJobOnTable(long dbId, long tableId) {
         for (IndexChangeJob indexChangeJob : 
ImmutableList.copyOf(indexChangeJobs.values())) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/DropIndexClause.java 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/DropIndexClause.java
index 822d0c875fa..51910d3cb19 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/DropIndexClause.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/DropIndexClause.java
@@ -20,9 +20,12 @@ package org.apache.doris.analysis;
 import org.apache.doris.alter.AlterOpType;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.info.PartitionNamesInfo;
 
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 public class DropIndexClause extends AlterTableClause {
@@ -32,12 +35,20 @@ public class DropIndexClause extends AlterTableClause {
 
     private boolean alter;
 
+    private final PartitionNamesInfo partitionNamesInfo;
+
     public DropIndexClause(String indexName, boolean ifExists, TableName 
tableName, boolean alter) {
+        this(indexName, ifExists, tableName, alter, null);
+    }
+
+    public DropIndexClause(String indexName, boolean ifExists, TableName 
tableName, boolean alter,
+            PartitionNamesInfo partitionNamesInfo) {
         super(AlterOpType.SCHEMA_CHANGE);
         this.indexName = indexName;
         this.ifExists = ifExists;
         this.tableName = tableName;
         this.alter = alter;
+        this.partitionNamesInfo = partitionNamesInfo;
     }
 
     public String getIndexName() {
@@ -56,6 +67,16 @@ public class DropIndexClause extends AlterTableClause {
         return alter;
     }
 
+    public boolean hasPartitionSpec() {
+        return partitionNamesInfo != null;
+    }
+
+    public List<String> getPartitionNames() {
+        return partitionNamesInfo == null
+                ? Collections.emptyList()
+                : partitionNamesInfo.getPartitionNames();
+    }
+
     @Override
     public Map<String, String> getProperties() {
         return null;
@@ -66,6 +87,18 @@ public class DropIndexClause extends AlterTableClause {
         if (StringUtils.isEmpty(indexName)) {
             throw new AnalysisException("index name is excepted");
         }
+        if (partitionNamesInfo != null) {
+            if (partitionNamesInfo.isTemp()) {
+                throw new AnalysisException(
+                        "DROP INDEX ON PARTITION does not support temporary 
partitions");
+            }
+            if (partitionNamesInfo.getPartitionNames() == null
+                    || partitionNamesInfo.getPartitionNames().isEmpty()) {
+                throw new AnalysisException(
+                        "DROP INDEX ON PARTITION requires explicit partition 
names, "
+                                + "PARTITIONS (*) is not supported");
+            }
+        }
     }
 
     @Override
@@ -89,6 +122,9 @@ public class DropIndexClause extends AlterTableClause {
         if (!alter) {
             stringBuilder.append(" ON ").append(tableName.toSql());
         }
+        if (partitionNamesInfo != null) {
+            stringBuilder.append(" ").append(partitionNamesInfo.toSql());
+        }
         return stringBuilder.toString();
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 3606f10fc7b..413434741de 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -6567,14 +6567,26 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
     public Command visitDropIndex(DropIndexContext ctx) {
         String name = ctx.name.getText();
         TableNameInfo tableName = new 
TableNameInfo(visitMultipartIdentifier(ctx.tableName));
+        PartitionNamesInfo partitionNamesInfo = null;
+        if (ctx.partitionSpec() != null) {
+            Pair<Boolean, List<String>> partitionSpec = 
visitPartitionSpec(ctx.partitionSpec());
+            partitionNamesInfo = new PartitionNamesInfo(partitionSpec.first, 
partitionSpec.second);
+        }
         List<AlterTableOp> alterTableOps = Lists
-                .newArrayList(new DropIndexOp(name, ctx.EXISTS() != null, 
tableName, false));
+                .newArrayList(new DropIndexOp(name, ctx.EXISTS() != null, 
tableName, false,
+                        partitionNamesInfo));
         return new AlterTableCommand(tableName, alterTableOps);
     }
 
     @Override
     public AlterTableOp visitDropIndexClause(DropIndexClauseContext ctx) {
-        return new DropIndexOp(ctx.name.getText(), ctx.EXISTS() != null, null, 
true);
+        PartitionNamesInfo partitionNamesInfo = null;
+        if (ctx.partitionSpec() != null) {
+            Pair<Boolean, List<String>> partitionSpec = 
visitPartitionSpec(ctx.partitionSpec());
+            partitionNamesInfo = new PartitionNamesInfo(partitionSpec.first, 
partitionSpec.second);
+        }
+        return new DropIndexOp(ctx.name.getText(), ctx.EXISTS() != null, null, 
true,
+                partitionNamesInfo);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/DropIndexOp.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/DropIndexOp.java
index 23cb536d799..3c233604cbb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/DropIndexOp.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/DropIndexOp.java
@@ -22,11 +22,14 @@ import org.apache.doris.analysis.AlterTableClause;
 import org.apache.doris.analysis.DropIndexClause;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.info.PartitionNamesInfo;
 import org.apache.doris.info.TableNameInfo;
 import org.apache.doris.qe.ConnectContext;
 
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -38,12 +41,26 @@ public class DropIndexOp extends AlterTableOp {
 
     private boolean alter;
 
+    private final PartitionNamesInfo partitionNamesInfo;
+
+    /**
+     * DropIndexOp without partition spec.
+     */
     public DropIndexOp(String indexName, boolean ifExists, TableNameInfo 
tableName, boolean alter) {
+        this(indexName, ifExists, tableName, alter, null);
+    }
+
+    /**
+     * DropIndexOp with optional partition spec.
+     */
+    public DropIndexOp(String indexName, boolean ifExists, TableNameInfo 
tableName, boolean alter,
+            PartitionNamesInfo partitionNamesInfo) {
         super(AlterOpType.SCHEMA_CHANGE);
         this.indexName = indexName;
         this.ifExists = ifExists;
         this.tableName = tableName;
         this.alter = alter;
+        this.partitionNamesInfo = partitionNamesInfo;
     }
 
     public String getIndexName() {
@@ -62,6 +79,16 @@ public class DropIndexOp extends AlterTableOp {
         return alter;
     }
 
+    public boolean hasPartitionSpec() {
+        return partitionNamesInfo != null;
+    }
+
+    public List<String> getPartitionNames() {
+        return partitionNamesInfo == null
+                ? Collections.emptyList()
+                : partitionNamesInfo.getPartitionNames();
+    }
+
     @Override
     public Map<String, String> getProperties() {
         return null;
@@ -75,12 +102,24 @@ public class DropIndexOp extends AlterTableOp {
         if (tableName != null) {
             tableName.analyze(ctx);
         }
+        if (partitionNamesInfo != null) {
+            if (partitionNamesInfo.isTemp()) {
+                throw new AnalysisException(
+                        "DROP INDEX ON PARTITION does not support temporary 
partitions");
+            }
+            if (partitionNamesInfo.getPartitionNames() == null
+                    || partitionNamesInfo.getPartitionNames().isEmpty()) {
+                throw new AnalysisException(
+                        "DROP INDEX ON PARTITION requires explicit partition 
names, "
+                                + "PARTITIONS (*) is not supported");
+            }
+        }
     }
 
     @Override
     public AlterTableClause translateToLegacyAlterClause() {
         return new DropIndexClause(indexName, ifExists, tableName != null ? 
tableName.transferToTableName() : null,
-                alter);
+                alter, partitionNamesInfo);
     }
 
     @Override
@@ -100,6 +139,9 @@ public class DropIndexOp extends AlterTableOp {
         if (!alter) {
             stringBuilder.append(" ON ").append(tableName != null ? 
tableName.toSql() : null);
         }
+        if (partitionNamesInfo != null) {
+            stringBuilder.append(" ").append(partitionNamesInfo.toSql());
+        }
         return stringBuilder.toString();
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/alter/IndexChangeJobTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/alter/IndexChangeJobTest.java
index a0081b5b5df..be9f6a678b7 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/alter/IndexChangeJobTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/alter/IndexChangeJobTest.java
@@ -36,12 +36,17 @@ import org.apache.doris.catalog.Partition;
 import org.apache.doris.catalog.Partition.PartitionState;
 import org.apache.doris.catalog.Replica;
 import org.apache.doris.catalog.Tablet;
+import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.FeMetaVersion;
 import org.apache.doris.common.UserException;
+import org.apache.doris.info.PartitionNamesInfo;
 import org.apache.doris.info.TableNameInfo;
 import org.apache.doris.nereids.trees.plans.commands.CancelAlterTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateIndexOp;
+import org.apache.doris.nereids.trees.plans.commands.info.DropIndexOp;
+import org.apache.doris.nereids.trees.plans.commands.info.IndexDefinition;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.task.AgentTask;
 import org.apache.doris.task.AgentTaskQueue;
@@ -797,4 +802,130 @@ public class IndexChangeJobTest {
         schemaChangeHandler.runAfterCatalogReady();
         Assert.assertEquals(AlterJobV2.JobState.CANCELLED, 
jobV2.getJobState());
     }
+
+    @Test
+    public void testDropIndexOnPartitionValidateRejectsStarPartition() throws 
Exception {
+        // PARTITIONS (*) should be rejected: partitionNames is null
+        PartitionNamesInfo starPartition = new PartitionNamesInfo(true);
+        DropIndexOp dropIndexOp = new DropIndexOp("index1", false, null, true, 
starPartition);
+        try {
+            dropIndexOp.validate(new ConnectContext());
+            Assert.fail("Should throw AnalysisException for PARTITIONS (*)");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("PARTITIONS (*) is not 
supported"));
+        }
+    }
+
+    @Test
+    public void testDropIndexOnPartitionValidateRejectsTempPartition() throws 
Exception {
+        // TEMPORARY PARTITION should be rejected
+        PartitionNamesInfo tempPartition = new PartitionNamesInfo(true, 
Lists.newArrayList("p1"));
+        DropIndexOp dropIndexOp = new DropIndexOp("index1", false, null, true, 
tempPartition);
+        try {
+            dropIndexOp.validate(new ConnectContext());
+            Assert.fail("Should throw AnalysisException for TEMPORARY 
PARTITION");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("does not support 
temporary partitions"));
+        }
+    }
+
+    @Test
+    public void testDropIndexOnPartitionValidateAcceptsNormalPartition() 
throws Exception {
+        // Normal partition spec should pass validate
+        PartitionNamesInfo normalPartition = new PartitionNamesInfo(false, 
Lists.newArrayList("p1", "p2"));
+        DropIndexOp dropIndexOp = new DropIndexOp("index1", false, null, true, 
normalPartition);
+        // Should not throw
+        dropIndexOp.validate(new ConnectContext());
+        Assert.assertTrue(dropIndexOp.hasPartitionSpec());
+        Assert.assertEquals(2, dropIndexOp.getPartitionNames().size());
+        DropIndexClause clause = (DropIndexClause) 
dropIndexOp.translateToLegacyAlterClause();
+        Assert.assertTrue(clause.hasPartitionSpec());
+        Assert.assertEquals(normalPartition.getPartitionNames(), 
clause.getPartitionNames());
+    }
+
+    @Test
+    public void testDropIndexOnPartitionRejectsNonPartitionedTable() throws 
UserException {
+        // testTable1 uses SinglePartitionInfo (non-partitioned), so DROP 
INDEX ON PARTITION should fail
+        fakeEnv = new FakeEnv();
+        fakeEditLog = new FakeEditLog();
+        FakeEnv.setEnv(masterEnv);
+        SchemaChangeHandler schemaChangeHandler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        ArrayList<AlterClause> alterClauses = new ArrayList<>();
+        Database db = 
masterEnv.getInternalCatalog().getDbOrDdlException(CatalogTestUtil.testDbId1);
+        OlapTable olapTable = (OlapTable) 
db.getTableOrDdlException(CatalogTestUtil.testTableId1);
+        String indexName = "index1";
+        TableNameInfo tableName = new 
TableNameInfo(masterEnv.getInternalCatalog().getName(), db.getName(),
+                olapTable.getName());
+        // First create an inverted index
+        IndexDefinition indexDefinition = new IndexDefinition(indexName, false,
+                Lists.newArrayList(olapTable.getBaseSchema().get(1).getName()),
+                "INVERTED",
+                Maps.newHashMap(), "balabala");
+        CreateIndexOp createIndexOp = new CreateIndexOp(tableName, 
indexDefinition, false);
+        ConnectContext connectContext = new ConnectContext();
+        createIndexOp.validate(connectContext);
+        alterClauses.add(createIndexOp.translateToLegacyAlterClause());
+        schemaChangeHandler.process(alterClauses, db, olapTable);
+        Assert.assertEquals(1, olapTable.getIndexes().size());
+        alterClauses.clear();
+
+        // Now try DROP INDEX ON PARTITION on this non-partitioned table
+        PartitionNamesInfo partitionNamesInfo = new PartitionNamesInfo(false, 
Lists.newArrayList("p1"));
+        DropIndexOp dropIndexOp = new DropIndexOp(indexName, false, tableName, 
false, partitionNamesInfo);
+        dropIndexOp.validate(connectContext);
+        alterClauses.add(dropIndexOp.translateToLegacyAlterClause());
+        try {
+            schemaChangeHandler.process(alterClauses, db, olapTable);
+            Assert.fail("Should throw DdlException for non-partitioned table");
+        } catch (DdlException e) {
+            Assert.assertTrue(e.getMessage().contains("is not partitioned"));
+        }
+        // Index definition should still exist
+        Assert.assertEquals(1, olapTable.getIndexes().size());
+    }
+
+    @Test
+    public void testDropIndexOnPartitionRejectsNonExistentIndex() throws 
UserException {
+        fakeEnv = new FakeEnv();
+        fakeEditLog = new FakeEditLog();
+        FakeEnv.setEnv(masterEnv);
+        SchemaChangeHandler schemaChangeHandler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        ArrayList<AlterClause> alterClauses = new ArrayList<>();
+        Database db = 
masterEnv.getInternalCatalog().getDbOrDdlException(CatalogTestUtil.testDbId1);
+        OlapTable olapTable = (OlapTable) 
db.getTableOrDdlException(CatalogTestUtil.testTableId1);
+        TableNameInfo tableName = new 
TableNameInfo(masterEnv.getInternalCatalog().getName(), db.getName(),
+                olapTable.getName());
+
+        PartitionNamesInfo partitionNamesInfo = new PartitionNamesInfo(false, 
Lists.newArrayList("p1"));
+        DropIndexOp dropIndexOp = new DropIndexOp("non_existent_index", false, 
tableName, false, partitionNamesInfo);
+        dropIndexOp.validate(new ConnectContext());
+        alterClauses.add(dropIndexOp.translateToLegacyAlterClause());
+        try {
+            schemaChangeHandler.process(alterClauses, db, olapTable);
+            Assert.fail("Should throw DdlException for non-existent index");
+        } catch (DdlException e) {
+            Assert.assertTrue(e.getMessage().contains("does not exist"));
+        }
+    }
+
+    @Test
+    public void testDropIndexOnPartitionIfExistsNonExistentIndex() throws 
UserException {
+        // IF EXISTS with non-existent index and partition spec should 
silently succeed
+        fakeEnv = new FakeEnv();
+        fakeEditLog = new FakeEditLog();
+        FakeEnv.setEnv(masterEnv);
+        SchemaChangeHandler schemaChangeHandler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        ArrayList<AlterClause> alterClauses = new ArrayList<>();
+        Database db = 
masterEnv.getInternalCatalog().getDbOrDdlException(CatalogTestUtil.testDbId1);
+        OlapTable olapTable = (OlapTable) 
db.getTableOrDdlException(CatalogTestUtil.testTableId1);
+        TableNameInfo tableName = new 
TableNameInfo(masterEnv.getInternalCatalog().getName(), db.getName(),
+                olapTable.getName());
+
+        PartitionNamesInfo partitionNamesInfo = new PartitionNamesInfo(false, 
Lists.newArrayList("p1"));
+        DropIndexOp dropIndexOp = new DropIndexOp("non_existent_index", true, 
tableName, false, partitionNamesInfo);
+        dropIndexOp.validate(new ConnectContext());
+        alterClauses.add(dropIndexOp.translateToLegacyAlterClause());
+        // IF EXISTS succeeds without changing metadata.
+        schemaChangeHandler.process(alterClauses, db, olapTable);
+    }
 }
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 ecf6a1be2cc..c9afc5f5d07 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
@@ -26,7 +26,10 @@ import org.apache.doris.catalog.Index;
 import org.apache.doris.catalog.KeysType;
 import org.apache.doris.catalog.MaterializedIndexMeta;
 import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.PrimitiveType;
 import org.apache.doris.catalog.Table;
+import org.apache.doris.common.Config;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.jmockit.Deencapsulation;
 import org.apache.doris.indexpolicy.DropIndexPolicyLog;
@@ -56,6 +59,8 @@ import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 public class SchemaChangeHandlerTest extends TestWithFeService {
     private static final Logger LOG = 
LogManager.getLogger(SchemaChangeHandlerTest.class);
@@ -853,6 +858,179 @@ public class SchemaChangeHandlerTest extends 
TestWithFeService {
         }
     }
 
+    private OlapTable createPartitionDropAdmissionTable(String tableName) 
throws Exception {
+        dropTable("test." + tableName, false);
+        createTable("CREATE TABLE test." + tableName + " ("
+                + "k1 INT, v1 STRING, INDEX idx_v1 (v1) USING INVERTED) "
+                + "DUPLICATE KEY(k1) "
+                + "PARTITION BY RANGE(k1) (PARTITION p1 VALUES LESS THAN (10),"
+                + " PARTITION p2 VALUES LESS THAN (20)) "
+                + "DISTRIBUTED BY HASH(k1) BUCKETS 1 "
+                + "PROPERTIES ('replication_num' = '1', 'light_schema_change' 
= 'true');");
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        return (OlapTable) db.getTableOrMetaException(tableName, 
Table.TableType.OLAP);
+    }
+
+    @Test
+    public void testPartitionDropRequiresLightIndexChanges() throws Exception {
+        String tableName = "sc_partition_drop_disabled";
+        OlapTable tbl = createPartitionDropAdmissionTable(tableName);
+        SchemaChangeHandler handler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        int jobsBefore = handler.getIndexChangeJobs().size();
+        int schemaVersion = 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion();
+        boolean saved = Config.enable_light_index_change;
+        try {
+            Config.enable_light_index_change = false;
+            Exception error = Assertions.assertThrows(Exception.class,
+                    () -> alterTable("ALTER TABLE test." + tableName
+                            + " DROP INDEX idx_v1 PARTITION (p1)", 
connectContext));
+            Assertions.assertTrue(error.getMessage().contains("requires 
enable_light_index_change"),
+                    error.getMessage());
+            Assertions.assertEquals(jobsBefore, 
handler.getIndexChangeJobs().size());
+            Assertions.assertEquals(schemaVersion,
+                    
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion());
+            Assertions.assertEquals(1, tbl.getIndexes().size());
+        } finally {
+            Config.enable_light_index_change = saved;
+        }
+    }
+
+    @Test
+    public void testPartitionDropChecksAllConflictsBeforePublishingJobs() 
throws Exception {
+        String tableName = "sc_partition_drop_late_conflict";
+        OlapTable tbl = createPartitionDropAdmissionTable(tableName);
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+        SchemaChangeHandler handler = 
Env.getCurrentEnv().getSchemaChangeHandler();
+        List<Partition> partitions = Lists.newArrayList(tbl.getPartitions());
+        Assertions.assertEquals(2, partitions.size());
+        Partition busyPartition = partitions.get(partitions.size() - 1);
+        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.setPartitionId(busyPartition.getId());
+        job.setPartitionName(busyPartition.getName());
+        job.setAlterInvertedIndexInfo(false, 
Lists.newArrayList(tbl.getIndexes().get(0)));
+        handler.getIndexChangeJobs().put(jobId, job);
+        Set<Long> jobsBefore = 
Sets.newHashSet(handler.getIndexChangeJobs().keySet());
+        try {
+            Assertions.assertThrows(Exception.class,
+                    () -> alterTable("ALTER TABLE test." + tableName
+                            + " DROP INDEX idx_v1 PARTITIONS (p1, p2)", 
connectContext));
+            Assertions.assertEquals(jobsBefore, 
handler.getIndexChangeJobs().keySet(),
+                    "A rejected statement must not publish a deletion job for 
an earlier partition");
+            Assertions.assertEquals(1, tbl.getIndexes().size());
+        } finally {
+            for (IndexChangeJob current : 
Lists.newArrayList(handler.getIndexChangeJobs().values())) {
+                if (!jobsBefore.contains(current.getJobId())) {
+                    current.setJobState(IndexChangeJob.JobState.CANCELLED);
+                    handler.getIndexChangeJobs().remove(current.getJobId());
+                }
+            }
+            handler.getIndexChangeJobs().remove(jobId);
+        }
+    }
+
+    @Test
+    public void testDropIndexOnPartitionsMustBeTheOnlyClause() throws 
Exception {
+        String tableName = "sc_drop_index_on_partitions_alone";
+        dropTable("test." + tableName, false);
+        createTable("CREATE TABLE test." + tableName + " (\n"
+                + "k1 INT,\n"
+                + "vi INT,\n"
+                + "v1 STRING,\n"
+                + "v2 STRING,\n"
+                + "INDEX idx_v1 (v1) USING INVERTED,\n"
+                + "INDEX idx_v2 (v2) USING INVERTED\n"
+                + ") ENGINE=OLAP\n"
+                + "DUPLICATE KEY(k1)\n"
+                + "PARTITION BY RANGE(k1) (PARTITION p1 VALUES LESS THAN 
(\"10\"),"
+                + " PARTITION p2 VALUES LESS THAN (\"20\"))\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();
+
+        // Reject mixed work before it can change columns, indexes, or 
partition scopes.
+        for (String clauses : Lists.newArrayList(
+                "MODIFY COLUMN vi BIGINT NULL, DROP INDEX idx_v1 PARTITION 
(p1)",
+                "ADD COLUMN x INT, DROP INDEX idx_v1 PARTITION (p1)",
+                "DROP INDEX idx_v1 PARTITION (p1), DROP INDEX idx_v2",
+                "DROP INDEX idx_v1 PARTITION (p1), DROP INDEX idx_v2 PARTITION 
(p2)")) {
+            String sql = "ALTER TABLE test." + tableName + " " + clauses;
+            Exception e = Assertions.assertThrows(Exception.class, () -> 
alterTable(sql, connectContext));
+            Assertions.assertTrue(e.getMessage().contains("DROP INDEX on 
partitions can not be combined"),
+                    e.getMessage());
+        }
+        Assertions.assertEquals(PrimitiveType.INT, 
tbl.getColumn("vi").getType().getPrimitiveType());
+        Assertions.assertNull(tbl.getColumn("x"));
+        Assertions.assertEquals(Lists.newArrayList("idx_v1", "idx_v2"),
+                
tbl.getIndexes().stream().map(Index::getIndexName).collect(Collectors.toList()));
+        Assertions.assertEquals(schemaVersion, 
tbl.getIndexMetaByIndexId(tbl.getBaseIndexId()).getSchemaVersion());
+        Assertions.assertEquals(indexChangeJobCount, 
handler.getIndexChangeJobs().size());
+
+        // Alone, it still submits an index job for the partition and keeps 
the index in the table metadata.
+        alterTable("ALTER TABLE test." + tableName + " DROP INDEX idx_v1 
PARTITION (p1)", connectContext);
+        Assertions.assertEquals(indexChangeJobCount + 1, 
handler.getIndexChangeJobs().size());
+        Assertions.assertEquals(Lists.newArrayList("idx_v1", "idx_v2"),
+                
tbl.getIndexes().stream().map(Index::getIndexName).collect(Collectors.toList()));
+    }
+
+    @Test
+    public void testDropIndexRejectedByPendingBuildOfSameIndex() throws 
Exception {
+        String tableName = "sc_drop_index_pending_build";
+        dropTable("test." + tableName, false);
+        createTable("CREATE TABLE test." + tableName + " (\n"
+                + "k1 INT,\n"
+                + "v1 STRING,\n"
+                + "INDEX idx_v1 (v1) USING INVERTED\n"
+                + ") ENGINE=OLAP\n"
+                + "DUPLICATE KEY(k1)\n"
+                + "PARTITION BY RANGE(k1) (PARTITION p1 VALUES LESS THAN 
(\"10\"),"
+                + " PARTITION p2 VALUES LESS THAN (\"20\"))\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();
+        Index index = tbl.getIndexes().stream()
+                .filter(idx -> idx.getIndexName().equals("idx_v1"))
+                .findFirst().get();
+
+        // Register an unfinished build job without scheduling it.
+        // A later build must not restore a dropped index.
+        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("p1");
+        job.setAlterInvertedIndexInfo(false, Lists.newArrayList(index));
+        handler.getIndexChangeJobs().put(jobId, job);
+        try {
+            String[][] clauseAndError = {
+                    {"DROP INDEX idx_v1", "has an unfinished index change 
job"},
+                    {"DROP INDEX idx_v1 PARTITION (p1)", "has an unfinished 
index change job"}};
+            for (String[] ce : clauseAndError) {
+                String sql = "ALTER TABLE test." + tableName + " " + ce[0];
+                Exception e = Assertions.assertThrows(Exception.class, () -> 
alterTable(sql, connectContext));
+                Assertions.assertTrue(e.getMessage().contains(ce[1]), 
e.getMessage());
+            }
+            Assertions.assertTrue(tbl.getIndexes().stream().anyMatch(idx -> 
idx.getIndexName().equals("idx_v1")));
+
+            job.setJobState(IndexChangeJob.JobState.FINISHED);
+            alterTable("ALTER TABLE test." + tableName + " DROP INDEX idx_v1", 
connectContext);
+            jobSize++;
+            Assertions.assertTrue(tbl.getIndexes().stream().noneMatch(idx -> 
idx.getIndexName().equals("idx_v1")));
+        } finally {
+            handler.getIndexChangeJobs().remove(jobId);
+        }
+    }
+
     @Test
     public void testAddInvertedIndexStoresCanonicalBuiltinAnalyzer() throws 
Exception {
         createAnalyzerAliasTable("sc_ik_alias");
diff --git 
a/regression-test/suites/inverted_index_p0/index_change/test_drop_index_on_partition.groovy
 
b/regression-test/suites/inverted_index_p0/index_change/test_drop_index_on_partition.groovy
new file mode 100644
index 00000000000..3ebdb824a59
--- /dev/null
+++ 
b/regression-test/suites/inverted_index_p0/index_change/test_drop_index_on_partition.groovy
@@ -0,0 +1,269 @@
+// 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.
+
+
+suite("test_drop_index_on_partition", "inverted_index") {
+    def timeout = 300000
+    def delta_time = 1000
+
+    def get_build_index_job_count = { table_name ->
+        def res = sql """SHOW BUILD INDEX WHERE TableName = "${table_name}";"""
+        return res.size()
+    }
+
+    def wait_for_build_index_on_partition_finish = { table_name, 
expected_job_count, OpTimeout ->
+        assertTrue(expected_job_count > 0, "No index change jobs were created 
for ${table_name}")
+        def finished = false
+        for (int t = delta_time; t <= OpTimeout; t += delta_time) {
+            def alter_res = sql """SHOW BUILD INDEX WHERE TableName = 
"${table_name}";"""
+            assertTrue(!alter_res.any { it[7] == "CANCELLED" },
+                    "An index change job was cancelled: ${alter_res}")
+            if (alter_res.size() >= expected_job_count && alter_res.every { 
it[7] == "FINISHED" }) {
+                logger.info(table_name + " all index change jobs finished: " + 
alter_res)
+                finished = true
+                break
+            }
+            sleep(delta_time)
+        }
+        assertTrue(finished, "Index change jobs did not finish within 
${OpTimeout} ms for ${table_name}")
+    }
+
+    // case 1: basic DROP INDEX ON PARTITION with job count verification
+    def tableName1 = "test_drop_idx_on_partition_basic"
+    sql "DROP TABLE IF EXISTS ${tableName1}"
+    sql """
+        CREATE TABLE IF NOT EXISTS ${tableName1} (
+            k1 int NOT NULL,
+            v1 varchar(50),
+            v2 string
+        )
+        DUPLICATE KEY(`k1`)
+        PARTITION BY RANGE(`k1`) (
+            PARTITION p1 VALUES LESS THAN (100),
+            PARTITION p2 VALUES LESS THAN (200),
+            PARTITION p3 VALUES LESS THAN (300)
+        )
+        DISTRIBUTED BY HASH(`k1`) BUCKETS 3
+        properties("replication_num" = "1");
+    """
+
+    sql "INSERT INTO ${tableName1} VALUES (10, 'hello world', 'foo bar'), (50, 
'test data', 'baz qux')"
+    sql "INSERT INTO ${tableName1} VALUES (110, 'hello doris', 'abc def'), 
(150, 'test query', 'ghi jkl')"
+    sql "INSERT INTO ${tableName1} VALUES (210, 'hello index', 'mno pqr'), 
(250, 'test drop', 'stu vwx')"
+
+    // create inverted index
+    sql "CREATE INDEX idx_v2 ON ${tableName1}(v2) USING INVERTED"
+    // Build physical files before testing partition deletion.
+    build_index_on_table("idx_v2", tableName1)
+    wait_for_build_index_on_partition_finish(tableName1, 
get_build_index_job_count(tableName1), timeout)
+
+    // verify index exists
+    def show_idx = sql "SHOW INDEX FROM ${tableName1}"
+    logger.info("show index: " + show_idx)
+    assertTrue(show_idx.toString().contains("idx_v2"))
+
+    // record job count before DROP
+    def job_count_before = get_build_index_job_count(tableName1)
+    logger.info("job count before drop: " + job_count_before)
+
+    // drop index on partition p1 only — should create new job(s)
+    sql "DROP INDEX idx_v2 ON ${tableName1} PARTITION (p1)"
+
+    def job_count_after = get_build_index_job_count(tableName1)
+    logger.info("job count after drop: " + job_count_after)
+    assertTrue(job_count_after > job_count_before,
+            "DROP INDEX ON PARTITION should create new build index jobs, " +
+            "before: ${job_count_before}, after: ${job_count_after}")
+
+    wait_for_build_index_on_partition_finish(tableName1, job_count_after, 
timeout)
+
+    // verify index definition still exists in table schema
+    show_idx = sql "SHOW INDEX FROM ${tableName1}"
+    logger.info("show index after drop on partition: " + show_idx)
+    assertTrue(show_idx.toString().contains("idx_v2"))
+
+    // query with fallback=true should return correct results on all partitions
+    sql """set enable_fallback_on_missing_inverted_index = true"""
+    def result1 = sql """SELECT * FROM ${tableName1} WHERE v2 = 'foo bar' 
ORDER BY k1"""
+    assertEquals(1, result1.size())
+    assertEquals(10, result1[0][0])
+
+    def result2 = sql """SELECT * FROM ${tableName1} WHERE v2 = 'abc def' 
ORDER BY k1"""
+    assertEquals(1, result2.size())
+    assertEquals(110, result2[0][0])
+
+    // case 2: drop index on multiple partitions with job count verification
+    def tableName2 = "test_drop_idx_on_multi_partitions"
+    sql "DROP TABLE IF EXISTS ${tableName2}"
+    sql """
+        CREATE TABLE IF NOT EXISTS ${tableName2} (
+            k1 int NOT NULL,
+            v1 varchar(50)
+        )
+        DUPLICATE KEY(`k1`)
+        PARTITION BY RANGE(`k1`) (
+            PARTITION p1 VALUES LESS THAN (100),
+            PARTITION p2 VALUES LESS THAN (200),
+            PARTITION p3 VALUES LESS THAN (300)
+        )
+        DISTRIBUTED BY HASH(`k1`) BUCKETS 3
+        properties("replication_num" = "1");
+    """
+
+    sql "INSERT INTO ${tableName2} VALUES (10, 'hello'), (110, 'world'), (210, 
'doris')"
+
+    sql "CREATE INDEX idx_v1 ON ${tableName2}(v1) USING INVERTED"
+    build_index_on_table("idx_v1", tableName2)
+    wait_for_build_index_on_partition_finish(tableName2, 
get_build_index_job_count(tableName2), timeout)
+
+    job_count_before = get_build_index_job_count(tableName2)
+
+    // drop index on p1 and p2 — should create at least 2 new jobs
+    sql "DROP INDEX idx_v1 ON ${tableName2} PARTITIONS (p1, p2)"
+
+    job_count_after = get_build_index_job_count(tableName2)
+    logger.info("multi-partition drop: job count before=${job_count_before}, 
after=${job_count_after}")
+    assertTrue(job_count_after >= job_count_before + 2,
+            "DROP INDEX ON 2 PARTITIONS should create at least 2 new jobs")
+
+    wait_for_build_index_on_partition_finish(tableName2, job_count_after, 
timeout)
+
+    // index definition should still exist
+    show_idx = sql "SHOW INDEX FROM ${tableName2}"
+    assertTrue(show_idx.toString().contains("idx_v1"))
+
+    // queries should return correct results with fallback
+    sql """set enable_fallback_on_missing_inverted_index = true"""
+    def result3 = sql """SELECT * FROM ${tableName2} ORDER BY k1"""
+    assertEquals(3, result3.size())
+
+    // case 3: error - non-partitioned table
+    def tableName3 = "test_drop_idx_on_partition_non_partitioned"
+    sql "DROP TABLE IF EXISTS ${tableName3}"
+    sql """
+        CREATE TABLE IF NOT EXISTS ${tableName3} (
+            k1 int NOT NULL,
+            v1 varchar(50)
+        )
+        DUPLICATE KEY(`k1`)
+        DISTRIBUTED BY HASH(`k1`) BUCKETS 3
+        properties("replication_num" = "1");
+    """
+    sql "INSERT INTO ${tableName3} VALUES (1, 'test')"
+    sql "CREATE INDEX idx_v1 ON ${tableName3}(v1) USING INVERTED"
+    build_index_on_table("idx_v1", tableName3)
+    wait_for_build_index_on_partition_finish(tableName3, 
get_build_index_job_count(tableName3), timeout)
+
+    test {
+        sql "DROP INDEX idx_v1 ON ${tableName3} PARTITION (p1)"
+        exception "is not partitioned"
+    }
+
+    // case 4: error - non-inverted index type
+    def tableName4 = "test_drop_idx_on_partition_non_inverted"
+    sql "DROP TABLE IF EXISTS ${tableName4}"
+    sql """
+        CREATE TABLE IF NOT EXISTS ${tableName4} (
+            k1 int NOT NULL,
+            v1 varchar(50)
+        )
+        DUPLICATE KEY(`k1`)
+        PARTITION BY RANGE(`k1`) (
+            PARTITION p1 VALUES LESS THAN (100),
+            PARTITION p2 VALUES LESS THAN (200)
+        )
+        DISTRIBUTED BY HASH(`k1`) BUCKETS 3
+        properties("replication_num" = "1");
+    """
+    sql "INSERT INTO ${tableName4} VALUES (10, 'test'), (110, 'data')"
+    sql "CREATE INDEX idx_ngram ON ${tableName4}(v1) USING NGRAM_BF 
PROPERTIES(\"gram_size\"=\"3\", \"bf_size\"=\"1024\")"
+    wait_for_last_col_change_finish(tableName4, timeout)
+
+    test {
+        sql "DROP INDEX idx_ngram ON ${tableName4} PARTITION (p1)"
+        exception "Only inverted index supports DROP INDEX ON PARTITION"
+    }
+
+    // case 5: error - non-existent partition
+    test {
+        sql "DROP INDEX idx_v2 ON ${tableName1} PARTITION 
(non_existent_partition)"
+        exception "does not exist"
+    }
+
+    // case 6: error - non-existent index
+    test {
+        sql "DROP INDEX non_existent_idx ON ${tableName1} PARTITION (p1)"
+        exception "does not exist"
+    }
+
+    // case 7: IF EXISTS with non-existent index should succeed silently
+    sql "DROP INDEX IF EXISTS non_existent_idx ON ${tableName1} PARTITION (p1)"
+
+    // case 8: error - PARTITIONS (*) not supported
+    test {
+        sql "DROP INDEX idx_v2 ON ${tableName1} PARTITIONS (*)"
+        exception "PARTITIONS (*) is not supported"
+    }
+
+    // case 9: error - TEMPORARY PARTITION not supported
+    test {
+        sql "DROP INDEX idx_v2 ON ${tableName1} TEMPORARY PARTITION (p1)"
+        exception "does not support temporary partitions"
+    }
+
+    // case 10: ALTER TABLE form with job count verification
+    def tableName5 = "test_drop_idx_on_partition_alter"
+    sql "DROP TABLE IF EXISTS ${tableName5}"
+    sql """
+        CREATE TABLE IF NOT EXISTS ${tableName5} (
+            k1 int NOT NULL,
+            v1 string
+        )
+        DUPLICATE KEY(`k1`)
+        PARTITION BY RANGE(`k1`) (
+            PARTITION p1 VALUES LESS THAN (100),
+            PARTITION p2 VALUES LESS THAN (200)
+        )
+        DISTRIBUTED BY HASH(`k1`) BUCKETS 3
+        properties("replication_num" = "1");
+    """
+    sql "INSERT INTO ${tableName5} VALUES (10, 'hello world'), (110, 'test 
data')"
+
+    sql "CREATE INDEX idx_v1 ON ${tableName5}(v1) USING INVERTED"
+    build_index_on_table("idx_v1", tableName5)
+    wait_for_build_index_on_partition_finish(tableName5, 
get_build_index_job_count(tableName5), timeout)
+
+    job_count_before = get_build_index_job_count(tableName5)
+
+    // use ALTER TABLE form
+    sql "ALTER TABLE ${tableName5} DROP INDEX idx_v1 PARTITION (p1)"
+
+    job_count_after = get_build_index_job_count(tableName5)
+    assertTrue(job_count_after > job_count_before,
+            "ALTER TABLE DROP INDEX ON PARTITION should create new jobs")
+
+    wait_for_build_index_on_partition_finish(tableName5, job_count_after, 
timeout)
+
+    // index definition should still exist
+    show_idx = sql "SHOW INDEX FROM ${tableName5}"
+    assertTrue(show_idx.toString().contains("idx_v1"))
+
+    // query all data with fallback
+    sql """set enable_fallback_on_missing_inverted_index = true"""
+    def result4 = sql """SELECT * FROM ${tableName5} ORDER BY k1"""
+    assertEquals(2, result4.size())
+}


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

Reply via email to