u70b3 commented on code in PR #67630:
URL: https://github.com/apache/doris/pull/67630#discussion_r4003111917


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowLanceIndexJobsCommand.java:
##########
@@ -0,0 +1,292 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.RedirectStatus;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.lance.job.LanceIndexJob;
+import org.apache.doris.datasource.lance.job.LanceIndexJobMutationState;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.analyzer.UnboundSlot;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.CompoundPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * SHOW LANCE INDEX JOBS [FROM [catalog.]db] [WHERE TableName = "tbl" [AND 
State = "PENDING"]].
+ *
+ * <p>Lists the durable Lance index job records held by the master. Rows whose 
persisted
+ * target no longer resolves (the catalog is gone, or the catalog is there but 
the db or
+ * table no longer resolves) are visible to global ADMIN only; every other row 
requires
+ * table-level SHOW on the persisted (catalog, db, table). Rows that fail the 
check are
+ * omitted entirely, so non-ADMIN users see no orphan trace, not even a count. 
The job
+ * locator, provider, normalized names, propertiesJson and schema contract are 
never shown.
+ *
+ * <p>The WHERE clause is deliberately narrowed to EqualTo predicates combined 
with AND
+ * over the case-insensitive keys TableName and State (no Like, unlike the 
SHOW COPY
+ * precedent).
+ */
+public class ShowLanceIndexJobsCommand extends ShowCommand {
+    public static final ImmutableList<String> TITLE_NAMES = new 
ImmutableList.Builder<String>()
+            .add("JobId")
+            .add("CatalogName")
+            .add("DbName")
+            .add("TableName")
+            .add("IndexName")
+            .add("Operation")
+            .add("State")
+            .add("RefreshState")
+            .add("PossibleLive")
+            .add("CreateTime")
+            .add("UpdateTime")
+            .add("Message")
+            .add("ForceReleased")
+            .add("ForceActor")
+            .add("ForceTime")
+            .build();
+
+    private static final String KEY_TABLE_NAME = "TableName";
+    private static final String KEY_STATE = "State";
+    private static final String WHERE_HINT = "Where clause should looks like: 
TableName = \"your_table_name\""
+            + " or State = 
\"PENDING|RUNNING|COMMITTED|NOT_COMMITTED|UNKNOWN\","
+            + " or compound predicate with operator AND";
+
+    private final List<String> nameParts;
+    private final Expression whereClause;
+
+    private String ctlName;
+    private String dbName;
+    private String tableNameValue;
+    private String stateValue;
+
+    public ShowLanceIndexJobsCommand(List<String> nameParts, Expression 
whereClause) {
+        super(PlanType.SHOW_LANCE_INDEX_JOBS_COMMAND);
+        this.nameParts = nameParts;
+        this.whereClause = whereClause;
+    }
+
+    public List<String> getNameParts() {
+        return nameParts;
+    }
+
+    public Expression getWhereClause() {
+        return whereClause;
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        ShowResultSetMetaData.Builder builder = 
ShowResultSetMetaData.builder();
+        for (String title : TITLE_NAMES) {
+            builder.addColumn(new Column(title, ScalarType.createVarchar(30)));
+        }
+        return builder.build();
+    }
+
+    private void validate(ConnectContext ctx) throws AnalysisException {
+        if (nameParts != null) {
+            if (nameParts.size() == 1) {
+                dbName = nameParts.get(0);
+                CatalogIf currentCatalog = ctx.getCurrentCatalog();
+                ctlName = currentCatalog == null ? null : 
currentCatalog.getName();
+            } else if (nameParts.size() == 2) {
+                ctlName = nameParts.get(0);
+                dbName = nameParts.get(1);
+            } else {
+                throw new AnalysisException(
+                        "Only support SHOW LANCE INDEX JOBS FROM 
[catalog.]database, but get: " + nameParts);
+            }
+        }
+        analyzeWhereClause();
+    }
+
+    private void analyzeWhereClause() throws AnalysisException {
+        if (whereClause == null) {
+            return;
+        }
+        List<Expression> children = new ArrayList<>();
+        splitCompoundPredicate(whereClause, children);
+        Set<String> names = new HashSet<>();
+        for (Expression child : children) {
+            analyzeSubPredicate(child);
+            String name = ((UnboundSlot) 
child.child(0)).getName().toLowerCase(Locale.ROOT);
+            if (!names.add(name)) {
+                throw new AnalysisException("column names on both sides of 
operator AND should be different");
+            }
+        }
+    }
+
+    private void splitCompoundPredicate(Expression expr, List<Expression> 
children) throws AnalysisException {
+        if (expr instanceof CompoundPredicate) {
+            if (!(expr instanceof And)) {
+                throw new AnalysisException("Only allow compound predicate 
with operator AND");
+            }
+            splitCompoundPredicate(expr.child(0), children);
+            splitCompoundPredicate(expr.child(1), children);
+        } else {
+            children.add(expr);
+        }
+    }
+
+    private void analyzeSubPredicate(Expression expr) throws AnalysisException 
{
+        if (!(expr instanceof EqualTo)
+                || !(expr.child(0) instanceof UnboundSlot)
+                || !(expr.child(1) instanceof StringLikeLiteral)) {
+            throw new AnalysisException(WHERE_HINT);
+        }
+        String key = ((UnboundSlot) expr.child(0)).getName();
+        String value = ((StringLikeLiteral) expr.child(1)).getStringValue();
+        if (key.equalsIgnoreCase(KEY_TABLE_NAME)) {
+            tableNameValue = value;
+        } else if (key.equalsIgnoreCase(KEY_STATE)) {
+            stateValue = value.toUpperCase(Locale.ROOT);
+            try {
+                LanceIndexJobMutationState.valueOf(stateValue);
+            } catch (IllegalArgumentException e) {
+                throw new AnalysisException("Unknown Lance index job state: " 
+ value + "; " + WHERE_HINT);
+            }
+        } else {
+            throw new AnalysisException(WHERE_HINT);
+        }
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        validate(ctx);
+        List<List<String>> rows = new ArrayList<>();
+        for (LanceIndexJob job : 
Env.getCurrentEnv().getLanceIndexJobManager().getAllJobsSnapshot()) {
+            CatalogIf<? extends DatabaseIf<? extends TableIf>> catalog =
+                    
Env.getCurrentEnv().getCatalogMgr().getCatalog(job.getCatalogId());
+            if (!matchesFilters(catalog, job)) {
+                continue;
+            }
+            if (!isAuthorized(ctx, catalog, job)) {
+                continue;
+            }
+            rows.add(renderRow(job, catalog));
+        }
+        return new ShowResultSet(getMetaData(), rows);
+    }
+
+    private boolean matchesFilters(CatalogIf<? extends DatabaseIf<? extends 
TableIf>> catalog, LanceIndexJob job) {
+        if (ctlName != null && (catalog == null || 
!ctlName.equals(catalog.getName()))) {
+            return false;
+        }
+        if (dbName != null && !dbName.equals(job.getDbName())) {

Review Comment:
   Fixed in a53722f78c — the `FROM` database name is now resolved through the 
catalog's own name semantics before the equality filter.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowLanceIndexJobsCommand.java:
##########
@@ -0,0 +1,292 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.RedirectStatus;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.lance.job.LanceIndexJob;
+import org.apache.doris.datasource.lance.job.LanceIndexJobMutationState;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.analyzer.UnboundSlot;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.CompoundPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * SHOW LANCE INDEX JOBS [FROM [catalog.]db] [WHERE TableName = "tbl" [AND 
State = "PENDING"]].
+ *
+ * <p>Lists the durable Lance index job records held by the master. Rows whose 
persisted
+ * target no longer resolves (the catalog is gone, or the catalog is there but 
the db or
+ * table no longer resolves) are visible to global ADMIN only; every other row 
requires
+ * table-level SHOW on the persisted (catalog, db, table). Rows that fail the 
check are
+ * omitted entirely, so non-ADMIN users see no orphan trace, not even a count. 
The job
+ * locator, provider, normalized names, propertiesJson and schema contract are 
never shown.
+ *
+ * <p>The WHERE clause is deliberately narrowed to EqualTo predicates combined 
with AND
+ * over the case-insensitive keys TableName and State (no Like, unlike the 
SHOW COPY
+ * precedent).
+ */
+public class ShowLanceIndexJobsCommand extends ShowCommand {
+    public static final ImmutableList<String> TITLE_NAMES = new 
ImmutableList.Builder<String>()
+            .add("JobId")
+            .add("CatalogName")
+            .add("DbName")
+            .add("TableName")
+            .add("IndexName")
+            .add("Operation")
+            .add("State")
+            .add("RefreshState")
+            .add("PossibleLive")
+            .add("CreateTime")
+            .add("UpdateTime")
+            .add("Message")
+            .add("ForceReleased")
+            .add("ForceActor")
+            .add("ForceTime")
+            .build();
+
+    private static final String KEY_TABLE_NAME = "TableName";
+    private static final String KEY_STATE = "State";
+    private static final String WHERE_HINT = "Where clause should looks like: 
TableName = \"your_table_name\""
+            + " or State = 
\"PENDING|RUNNING|COMMITTED|NOT_COMMITTED|UNKNOWN\","
+            + " or compound predicate with operator AND";
+
+    private final List<String> nameParts;
+    private final Expression whereClause;
+
+    private String ctlName;
+    private String dbName;
+    private String tableNameValue;
+    private String stateValue;
+
+    public ShowLanceIndexJobsCommand(List<String> nameParts, Expression 
whereClause) {
+        super(PlanType.SHOW_LANCE_INDEX_JOBS_COMMAND);
+        this.nameParts = nameParts;
+        this.whereClause = whereClause;
+    }
+
+    public List<String> getNameParts() {
+        return nameParts;
+    }
+
+    public Expression getWhereClause() {
+        return whereClause;
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        ShowResultSetMetaData.Builder builder = 
ShowResultSetMetaData.builder();
+        for (String title : TITLE_NAMES) {
+            builder.addColumn(new Column(title, ScalarType.createVarchar(30)));
+        }
+        return builder.build();
+    }
+
+    private void validate(ConnectContext ctx) throws AnalysisException {
+        if (nameParts != null) {
+            if (nameParts.size() == 1) {
+                dbName = nameParts.get(0);
+                CatalogIf currentCatalog = ctx.getCurrentCatalog();
+                ctlName = currentCatalog == null ? null : 
currentCatalog.getName();
+            } else if (nameParts.size() == 2) {
+                ctlName = nameParts.get(0);
+                dbName = nameParts.get(1);
+            } else {
+                throw new AnalysisException(
+                        "Only support SHOW LANCE INDEX JOBS FROM 
[catalog.]database, but get: " + nameParts);
+            }
+        }
+        analyzeWhereClause();
+    }
+
+    private void analyzeWhereClause() throws AnalysisException {
+        if (whereClause == null) {
+            return;
+        }
+        List<Expression> children = new ArrayList<>();
+        splitCompoundPredicate(whereClause, children);
+        Set<String> names = new HashSet<>();
+        for (Expression child : children) {
+            analyzeSubPredicate(child);
+            String name = ((UnboundSlot) 
child.child(0)).getName().toLowerCase(Locale.ROOT);
+            if (!names.add(name)) {
+                throw new AnalysisException("column names on both sides of 
operator AND should be different");
+            }
+        }
+    }
+
+    private void splitCompoundPredicate(Expression expr, List<Expression> 
children) throws AnalysisException {
+        if (expr instanceof CompoundPredicate) {
+            if (!(expr instanceof And)) {
+                throw new AnalysisException("Only allow compound predicate 
with operator AND");
+            }
+            splitCompoundPredicate(expr.child(0), children);
+            splitCompoundPredicate(expr.child(1), children);
+        } else {
+            children.add(expr);
+        }
+    }
+
+    private void analyzeSubPredicate(Expression expr) throws AnalysisException 
{
+        if (!(expr instanceof EqualTo)
+                || !(expr.child(0) instanceof UnboundSlot)
+                || !(expr.child(1) instanceof StringLikeLiteral)) {
+            throw new AnalysisException(WHERE_HINT);
+        }
+        String key = ((UnboundSlot) expr.child(0)).getName();
+        String value = ((StringLikeLiteral) expr.child(1)).getStringValue();
+        if (key.equalsIgnoreCase(KEY_TABLE_NAME)) {
+            tableNameValue = value;
+        } else if (key.equalsIgnoreCase(KEY_STATE)) {
+            stateValue = value.toUpperCase(Locale.ROOT);
+            try {
+                LanceIndexJobMutationState.valueOf(stateValue);
+            } catch (IllegalArgumentException e) {
+                throw new AnalysisException("Unknown Lance index job state: " 
+ value + "; " + WHERE_HINT);
+            }
+        } else {
+            throw new AnalysisException(WHERE_HINT);
+        }
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        validate(ctx);
+        List<List<String>> rows = new ArrayList<>();
+        for (LanceIndexJob job : 
Env.getCurrentEnv().getLanceIndexJobManager().getAllJobsSnapshot()) {
+            CatalogIf<? extends DatabaseIf<? extends TableIf>> catalog =
+                    
Env.getCurrentEnv().getCatalogMgr().getCatalog(job.getCatalogId());
+            if (!matchesFilters(catalog, job)) {
+                continue;
+            }
+            if (!isAuthorized(ctx, catalog, job)) {
+                continue;
+            }
+            rows.add(renderRow(job, catalog));
+        }
+        return new ShowResultSet(getMetaData(), rows);
+    }
+
+    private boolean matchesFilters(CatalogIf<? extends DatabaseIf<? extends 
TableIf>> catalog, LanceIndexJob job) {
+        if (ctlName != null && (catalog == null || 
!ctlName.equals(catalog.getName()))) {
+            return false;
+        }
+        if (dbName != null && !dbName.equals(job.getDbName())) {
+            return false;
+        }
+        if (tableNameValue != null && 
!tableNameValue.equals(job.getTableName())) {
+            return false;
+        }
+        return stateValue == null
+                || job.getMutationState() != null && 
stateValue.equals(job.getMutationState().name());
+    }
+
+    /**
+     * Orphan and half-orphan rows (catalog gone, or persisted db/table no 
longer resolvable)
+     * are visible to global ADMIN only; every other row needs table-level 
SHOW on the
+     * persisted target. The caller omits the row when this returns false, so 
non-ADMIN users
+     * see no trace of orphaned jobs, not even a count.
+     */
+    static boolean isAuthorized(ConnectContext ctx, CatalogIf<? extends 
DatabaseIf<? extends TableIf>> catalog,
+            LanceIndexJob job) {
+        if (!targetResolves(catalog, job)) {
+            return Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, 
PrivPredicate.ADMIN);
+        }
+        return Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, 
catalog.getName(),
+                job.getDbName(), job.getTableName(), PrivPredicate.SHOW);
+    }
+
+    static boolean targetResolves(CatalogIf<? extends DatabaseIf<? extends 
TableIf>> catalog, LanceIndexJob job) {

Review Comment:
   Fixed in a53722f78c — authorization additionally revalidates the job's 
persisted locator against the catalog's currently resolved dataset target; a 
mismatch degrades to the orphan/ADMIN-only path.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowLanceIndexJobsCommand.java:
##########
@@ -0,0 +1,292 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.RedirectStatus;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.lance.job.LanceIndexJob;
+import org.apache.doris.datasource.lance.job.LanceIndexJobMutationState;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.analyzer.UnboundSlot;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.CompoundPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * SHOW LANCE INDEX JOBS [FROM [catalog.]db] [WHERE TableName = "tbl" [AND 
State = "PENDING"]].
+ *
+ * <p>Lists the durable Lance index job records held by the master. Rows whose 
persisted
+ * target no longer resolves (the catalog is gone, or the catalog is there but 
the db or
+ * table no longer resolves) are visible to global ADMIN only; every other row 
requires
+ * table-level SHOW on the persisted (catalog, db, table). Rows that fail the 
check are
+ * omitted entirely, so non-ADMIN users see no orphan trace, not even a count. 
The job
+ * locator, provider, normalized names, propertiesJson and schema contract are 
never shown.
+ *
+ * <p>The WHERE clause is deliberately narrowed to EqualTo predicates combined 
with AND
+ * over the case-insensitive keys TableName and State (no Like, unlike the 
SHOW COPY
+ * precedent).
+ */
+public class ShowLanceIndexJobsCommand extends ShowCommand {
+    public static final ImmutableList<String> TITLE_NAMES = new 
ImmutableList.Builder<String>()
+            .add("JobId")
+            .add("CatalogName")
+            .add("DbName")
+            .add("TableName")
+            .add("IndexName")
+            .add("Operation")
+            .add("State")
+            .add("RefreshState")
+            .add("PossibleLive")
+            .add("CreateTime")
+            .add("UpdateTime")
+            .add("Message")
+            .add("ForceReleased")
+            .add("ForceActor")
+            .add("ForceTime")
+            .build();
+
+    private static final String KEY_TABLE_NAME = "TableName";
+    private static final String KEY_STATE = "State";
+    private static final String WHERE_HINT = "Where clause should looks like: 
TableName = \"your_table_name\""
+            + " or State = 
\"PENDING|RUNNING|COMMITTED|NOT_COMMITTED|UNKNOWN\","
+            + " or compound predicate with operator AND";
+
+    private final List<String> nameParts;
+    private final Expression whereClause;
+
+    private String ctlName;
+    private String dbName;
+    private String tableNameValue;
+    private String stateValue;
+
+    public ShowLanceIndexJobsCommand(List<String> nameParts, Expression 
whereClause) {
+        super(PlanType.SHOW_LANCE_INDEX_JOBS_COMMAND);
+        this.nameParts = nameParts;
+        this.whereClause = whereClause;
+    }
+
+    public List<String> getNameParts() {
+        return nameParts;
+    }
+
+    public Expression getWhereClause() {
+        return whereClause;
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        ShowResultSetMetaData.Builder builder = 
ShowResultSetMetaData.builder();
+        for (String title : TITLE_NAMES) {
+            builder.addColumn(new Column(title, ScalarType.createVarchar(30)));
+        }
+        return builder.build();
+    }
+
+    private void validate(ConnectContext ctx) throws AnalysisException {
+        if (nameParts != null) {
+            if (nameParts.size() == 1) {
+                dbName = nameParts.get(0);
+                CatalogIf currentCatalog = ctx.getCurrentCatalog();
+                ctlName = currentCatalog == null ? null : 
currentCatalog.getName();
+            } else if (nameParts.size() == 2) {
+                ctlName = nameParts.get(0);
+                dbName = nameParts.get(1);
+            } else {
+                throw new AnalysisException(
+                        "Only support SHOW LANCE INDEX JOBS FROM 
[catalog.]database, but get: " + nameParts);
+            }
+        }
+        analyzeWhereClause();
+    }
+
+    private void analyzeWhereClause() throws AnalysisException {
+        if (whereClause == null) {
+            return;
+        }
+        List<Expression> children = new ArrayList<>();
+        splitCompoundPredicate(whereClause, children);
+        Set<String> names = new HashSet<>();
+        for (Expression child : children) {
+            analyzeSubPredicate(child);
+            String name = ((UnboundSlot) 
child.child(0)).getName().toLowerCase(Locale.ROOT);
+            if (!names.add(name)) {
+                throw new AnalysisException("column names on both sides of 
operator AND should be different");
+            }
+        }
+    }
+
+    private void splitCompoundPredicate(Expression expr, List<Expression> 
children) throws AnalysisException {
+        if (expr instanceof CompoundPredicate) {
+            if (!(expr instanceof And)) {
+                throw new AnalysisException("Only allow compound predicate 
with operator AND");
+            }
+            splitCompoundPredicate(expr.child(0), children);
+            splitCompoundPredicate(expr.child(1), children);
+        } else {
+            children.add(expr);
+        }
+    }
+
+    private void analyzeSubPredicate(Expression expr) throws AnalysisException 
{
+        if (!(expr instanceof EqualTo)
+                || !(expr.child(0) instanceof UnboundSlot)
+                || !(expr.child(1) instanceof StringLikeLiteral)) {
+            throw new AnalysisException(WHERE_HINT);
+        }
+        String key = ((UnboundSlot) expr.child(0)).getName();
+        String value = ((StringLikeLiteral) expr.child(1)).getStringValue();
+        if (key.equalsIgnoreCase(KEY_TABLE_NAME)) {
+            tableNameValue = value;
+        } else if (key.equalsIgnoreCase(KEY_STATE)) {
+            stateValue = value.toUpperCase(Locale.ROOT);
+            try {
+                LanceIndexJobMutationState.valueOf(stateValue);
+            } catch (IllegalArgumentException e) {
+                throw new AnalysisException("Unknown Lance index job state: " 
+ value + "; " + WHERE_HINT);
+            }
+        } else {
+            throw new AnalysisException(WHERE_HINT);
+        }
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        validate(ctx);
+        List<List<String>> rows = new ArrayList<>();
+        for (LanceIndexJob job : 
Env.getCurrentEnv().getLanceIndexJobManager().getAllJobsSnapshot()) {
+            CatalogIf<? extends DatabaseIf<? extends TableIf>> catalog =
+                    
Env.getCurrentEnv().getCatalogMgr().getCatalog(job.getCatalogId());
+            if (!matchesFilters(catalog, job)) {
+                continue;
+            }
+            if (!isAuthorized(ctx, catalog, job)) {
+                continue;
+            }
+            rows.add(renderRow(job, catalog));
+        }
+        return new ShowResultSet(getMetaData(), rows);
+    }
+
+    private boolean matchesFilters(CatalogIf<? extends DatabaseIf<? extends 
TableIf>> catalog, LanceIndexJob job) {
+        if (ctlName != null && (catalog == null || 
!ctlName.equals(catalog.getName()))) {
+            return false;
+        }
+        if (dbName != null && !dbName.equals(job.getDbName())) {
+            return false;
+        }
+        if (tableNameValue != null && 
!tableNameValue.equals(job.getTableName())) {
+            return false;
+        }
+        return stateValue == null
+                || job.getMutationState() != null && 
stateValue.equals(job.getMutationState().name());
+    }
+
+    /**
+     * Orphan and half-orphan rows (catalog gone, or persisted db/table no 
longer resolvable)
+     * are visible to global ADMIN only; every other row needs table-level 
SHOW on the
+     * persisted target. The caller omits the row when this returns false, so 
non-ADMIN users
+     * see no trace of orphaned jobs, not even a count.
+     */
+    static boolean isAuthorized(ConnectContext ctx, CatalogIf<? extends 
DatabaseIf<? extends TableIf>> catalog,
+            LanceIndexJob job) {
+        if (!targetResolves(catalog, job)) {
+            return Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, 
PrivPredicate.ADMIN);
+        }
+        return Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, 
catalog.getName(),
+                job.getDbName(), job.getTableName(), PrivPredicate.SHOW);
+    }
+
+    static boolean targetResolves(CatalogIf<? extends DatabaseIf<? extends 
TableIf>> catalog, LanceIndexJob job) {
+        if (catalog == null) {
+            return false;
+        }
+        DatabaseIf<? extends TableIf> db = 
catalog.getDbNullable(job.getDbName());

Review Comment:
   Fixed in a53722f78c — target resolution is exception-safe: any provider 
lookup failure counts as an unresolved target (fixed 5103 for detail, row 
omitted for list), so provider errors never leak through the authorization 
boundary.



##########
regression-test/suites/external_table_p0/lance/test_lance_index_admission.groovy:
##########
@@ -0,0 +1,257 @@
+// 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_lance_index_admission", "p0,external,nonConcurrent") {
+    // The Lance fixture is preinstalled in the MinIO container of the Iceberg
+    // external environment, so this suite deliberately shares its switch.
+    String enabled = context.config.otherConfigs.get("enableIcebergTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable Lance index admission test because the Iceberg 
MinIO environment is disabled.")
+        return
+    }
+
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+    String lanceRestPort = context.config.otherConfigs.get("lance_rest_port")
+    // Admitted jobs are durable and stay PENDING forever in this delivery 
slice: dispatch,

Review Comment:
   Not taking for this slice — the regression pipeline FE is per-run 
disposable, so CI runs don't accumulate against the global quota; the suite 
adopts RESOLVE-based cleanup when PR3E (#67754) lands — see 
https://github.com/apache/doris/pull/67630#issuecomment-5635777574.



-- 
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]


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

Reply via email to