ShowLanceIndexJobCommand.java
// 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.ErrorCode;
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.LanceIndexJobResult;
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.Collections;
import java.util.List;
/**
* SHOW LANCE INDEX JOB <jobId>.
*
* <p>Non-disclosing job detail (v5.1 section 8): the record is loaded first and authorized
* against its persisted target before any field is returned. Orphan and half-orphan targets
* (catalog gone, or persisted db/table no longer resolvable) require global ADMIN; a
* resolvable target requires table-level SHOW. A missing job and an unauthorized job share
* the same fixed ERR_LANCE_INDEX_JOB_NOT_FOUND response that names only the job id, so the
* existence and target of a job are never disclosed. The job locator, provider, normalized
* names, propertiesJson and schema contract contents are never shown.
*/
public class ShowLanceIndexJobCommand extends ShowCommand {
public static final ImmutableList<String> TITLE_NAMES = new ImmutableList.Builder<String>()
.addAll(ShowLanceIndexJobsCommand.TITLE_NAMES)
.add("Creator")
.add("ResultCode")
.add("CompletionReason")
.add("ExternalMetadataAdvanced")
.add("BackendId")
.add("BeProcessEpoch")
.add("InvocationId")
.add("DeadlineMs")
.add("TerminationProof")
.add("AdmittedDatasetVersion")
.add("SchemaContractVersion")
.add("Revision")
.add("ForceNote")
.add("ForceWarning")
.add("IfNotExists")
.add("IfExists")
.add("IndexType")
.add("ColumnName")
.build();
private final long jobId;
public ShowLanceIndexJobCommand(long jobId) {
super(PlanType.SHOW_LANCE_INDEX_JOB_COMMAND);
this.jobId = jobId;
}
public long getJobId() {
return jobId;
}
@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();
}
@Override
public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) throws Exception {
LanceIndexJob job = Env.getCurrentEnv().getLanceIndexJobManager().getJob(jobId);
if (job == null) {
throw notFound();
}
CatalogIf<? extends DatabaseIf<? extends TableIf>> catalog =
Env.getCurrentEnv().getCatalogMgr().getCatalog(job.getCatalogId());
// Missing and unauthorized jobs share the same fixed response; authorize before
// returning any field.
if (!ShowLanceIndexJobsCommand.isAuthorized(ctx, catalog, job)) {
throw notFound();
}
return new ShowResultSet(getMetaData(), Collections.singletonList(renderRow(job, catalog)));
}
private AnalysisException notFound() {
return new AnalysisException(ErrorCode.ERR_LANCE_INDEX_JOB_NOT_FOUND.formatErrorMsg(jobId),
ErrorCode.ERR_LANCE_INDEX_JOB_NOT_FOUND);
}
private static List<String> renderRow(LanceIndexJob job,
CatalogIf<? extends DatabaseIf<? extends TableIf>> catalog) {
List<String> row = new ArrayList<>(TITLE_NAMES.size());
row.add(String.valueOf(job.getJobId()));
// See ShowLanceIndexJobsCommand: the catalog name is not persisted on the job record,
// so an orphan row renders it empty rather than fabricating one from the internal id.
row.add(catalog == null ? "" : catalog.getName());
row.add(job.getDbName());
row.add(job.getTableName());
row.add(job.getDisplayIndexName());
row.add(job.getMutationType() == null ? "" : job.getMutationType().name());
row.add(job.getMutationState() == null ? "" : job.getMutationState().name());
row.add(job.getRefreshState() == null ? "" : job.getRefreshState().name());
row.add(job.holdsPossibleLiveSlot() ? "YES" : "NO");
row.add(TimeUtils.longToTimeString(job.getCreateTimeMs()));
row.add(TimeUtils.longToTimeString(job.getUpdateTimeMs()));
LanceIndexJobResult result = job.getResult();
// The result is always null until a worker reports (no worker exists yet); the same
// holds for every dispatch field. All of them render as empty strings, never NPE.
row.add(result == null || result.getSanitizedMessage() == null ? "" : result.getSanitizedMessage());
row.addAll(ShowLanceIndexJobsCommand.renderForceAudit(job));
row.add(job.getCreator() == null ? "" : job.getCreator());
row.add(result == null ? "" : result.getResultCode().name());
row.add(result == null || result.getCompletionReason() == null ? "" : result.getCompletionReason().name());
row.add(result == null ? "" : (result.isExternalMetadataAdvanced() ? "YES" : "NO"));
row.add(job.getBackendId() == null ? "" : String.valueOf(job.getBackendId()));
row.add(job.getBeProcessEpoch() == null ? "" : String.valueOf(job.getBeProcessEpoch()));
row.add(job.getInvocationId() == null ? "" : job.getInvocationId());
row.add(job.getDeadlineMs() == null ? "" : String.valueOf(job.getDeadlineMs()));
row.add(job.getTerminationProof() == null ? "" : job.getTerminationProof().name());
row.add(String.valueOf(job.getAdmittedDatasetVersion()));
row.add(job.getSchemaContract() == null
? "" : String.valueOf(job.getSchemaContract().getSchemaContractVersion()));
row.add(String.valueOf(job.getRevision()));
row.add(job.isForceReleased() && job.getForceNote() != null ? job.getForceNote() : "");
row.add(job.isForceReleased() && job.getForceWarning() != null ? job.getForceWarning() : "");
row.add(job.isIfNotExists() ? "YES" : "NO");
row.add(job.isIfExists() ? "YES" : "NO");
row.add(job.getIndexType() == null ? "" : job.getIndexType());
row.add(job.getColumnName() == null ? "" : job.getColumnName());
return row;
}
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitShowLanceIndexJobCommand(this, context);
}
@Override
public RedirectStatus toRedirectStatus() {
return RedirectStatus.FORWARD_NO_SYNC;
}
}