This is an automated email from the ASF dual-hosted git repository.
starocean999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 394af9c3cab [Enhancement] (nereids) implement backupCommand in
nereids (#50589)
394af9c3cab is described below
commit 394af9c3cabf9515fff8c466a09cff4175936a09
Author: yaoxiao <[email protected]>
AuthorDate: Thu Jun 12 10:50:11 2025 +0800
[Enhancement] (nereids) implement backupCommand in nereids (#50589)
Issue Number: #42830
---
.../antlr4/org/apache/doris/nereids/DorisParser.g4 | 6 +-
.../org/apache/doris/backup/BackupHandler.java | 205 +++++++++++++++
.../doris/nereids/parser/LogicalPlanBuilder.java | 25 ++
.../apache/doris/nereids/trees/TableSample.java | 4 +
.../apache/doris/nereids/trees/plans/PlanType.java | 1 +
.../trees/plans/commands/BackupCommand.java | 274 +++++++++++++++++++++
.../trees/plans/commands/info/TableRefInfo.java | 36 +++
.../trees/plans/visitor/CommandVisitor.java | 5 +
.../trees/plans/commands/BackupCommandTest.java | 125 ++++++++++
9 files changed, 678 insertions(+), 3 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 8780868b7ce..c56234dea7e 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
@@ -458,15 +458,15 @@ supportedOtherStatement
| INSTALL PLUGIN FROM source=identifierOrText properties=propertyClause?
#installPlugin
| UNINSTALL PLUGIN name=identifierOrText
#uninstallPlugin
| LOCK TABLES (lockTable (COMMA lockTable)*)?
#lockTables
+ | BACKUP SNAPSHOT label=multipartIdentifier TO repo=identifier
+ ((ON | EXCLUDE) LEFT_PAREN baseTableRef (COMMA baseTableRef)*
RIGHT_PAREN)?
+ properties=propertyClause?
#backup
;
unsupportedOtherStatement
: WARM UP (CLUSTER | COMPUTE GROUP) destination=identifier WITH
((CLUSTER | COMPUTE GROUP) source=identifier |
(warmUpItem (AND warmUpItem)*)) FORCE?
#warmUpCluster
- | BACKUP SNAPSHOT label=multipartIdentifier TO repo=identifier
- ((ON | EXCLUDE) LEFT_PAREN baseTableRef (COMMA baseTableRef)*
RIGHT_PAREN)?
- properties=propertyClause?
#backup
| RESTORE SNAPSHOT label=multipartIdentifier FROM repo=identifier
((ON | EXCLUDE) LEFT_PAREN baseTableRef (COMMA baseTableRef)*
RIGHT_PAREN)?
properties=propertyClause?
#restore
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/backup/BackupHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/backup/BackupHandler.java
index 294d9b3bed5..a79f962d747 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/backup/BackupHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/backup/BackupHandler.java
@@ -54,8 +54,11 @@ import org.apache.doris.fs.FileSystemFactory;
import org.apache.doris.fs.remote.AzureFileSystem;
import org.apache.doris.fs.remote.RemoteFileSystem;
import org.apache.doris.fs.remote.S3FileSystem;
+import org.apache.doris.nereids.trees.plans.commands.BackupCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelBackupCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateRepositoryCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.TableNameInfo;
+import org.apache.doris.nereids.trees.plans.commands.info.TableRefInfo;
import org.apache.doris.persist.BarrierLog;
import org.apache.doris.task.DirMoveTask;
import org.apache.doris.task.DownloadTask;
@@ -78,6 +81,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
@@ -381,6 +385,48 @@ public class BackupHandler extends MasterDaemon implements
Writable {
}
}
+ public void process(BackupCommand command) throws DdlException {
+ if (Config.isCloudMode()) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "BACKUP and RESTORE are not supported by the cloud mode
yet");
+ }
+
+ // check if repo exist
+ String repoName = command.getRepoName();
+ Repository repository = null;
+ if (!repoName.equals(Repository.KEEP_ON_LOCAL_REPO_NAME)) {
+ repository = repoMgr.getRepo(repoName);
+ if (repository == null) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "Repository " + repoName + " does not exist");
+ }
+ }
+
+ // check if db exist
+ String dbName = command.getDbName();
+ Database db = env.getInternalCatalog().getDbOrDdlException(dbName);
+
+ // Try to get sequence lock.
+ // We expect at most one operation on a repo at same time.
+ // But this operation may take a few seconds with lock held.
+ // So we use tryLock() to give up this operation if we can not get
lock.
+ tryLock();
+ try {
+ // Check if there is backup or restore job running on this database
+ AbstractJob currentJob = getCurrentJob(db.getId());
+ if (currentJob != null && !currentJob.isDone()) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "Can only run one backup or restore job of a database
at same time "
+ + ", current running: label = " +
currentJob.getLabel() + " jobId = "
+ + currentJob.getJobId() + ", to run label = " +
command.getLabel());
+ }
+
+ backup(repository, db, command);
+ } finally {
+ seqlock.unlock();
+ }
+ }
+
// the entry method of submitting a backup or restore job
public void process(AbstractBackupStmt stmt) throws DdlException {
if (Config.isCloudMode()) {
@@ -440,6 +486,165 @@ public class BackupHandler extends MasterDaemon
implements Writable {
}
}
+ private void backup(Repository repository, Database db, BackupCommand
command) throws DdlException {
+ if (repository != null && repository.isReadOnly()) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
"Repository " + repository.getName()
+ + " is read only");
+ }
+
+ long commitSeq = 0;
+ Set<String> tableNames = Sets.newHashSet();
+
+ List<TableRefInfo> tableRefInfos = command.getTableRefInfos();
+
+ // Obtain the snapshot commit seq, any creating table binlog will be
visible.
+ db.readLock();
+ try {
+ BarrierLog log = new BarrierLog(db.getId(), db.getFullName());
+ commitSeq = env.getEditLog().logBarrier(log);
+
+ // Determine the tables to be backed up
+ if (tableRefInfos.isEmpty()) {
+ tableNames = db.getTableNames();
+ } else if (command.isExclude()) {
+ tableNames = db.getTableNames();
+ for (TableRefInfo tableRefInfo : tableRefInfos) {
+ if
(!tableNames.remove(tableRefInfo.getTableNameInfo().getTbl())) {
+ LOG.info("exclude table " +
tableRefInfo.getTableNameInfo().getTbl()
+ + " of backup stmt is not exists in db " +
db.getFullName());
+ }
+ }
+ }
+ } finally {
+ db.readUnlock();
+ }
+
+ List<TableRef> tblRefs = Lists.newArrayList();
+ if (!tableRefInfos.isEmpty() && !command.isExclude()) {
+ for (TableRefInfo tableRefInfo : tableRefInfos) {
+ tblRefs.add(tableRefInfo.translateToLegacyTableRef());
+ }
+ } else {
+ for (String tableName : tableNames) {
+ TableRefInfo tableRefInfo = new TableRefInfo(new
TableNameInfo(db.getFullName(), tableName),
+ null,
+ null,
+ null,
+ new ArrayList<>(),
+ null,
+ null,
+ new ArrayList<>());
+ tblRefs.add(tableRefInfo.translateToLegacyTableRef());
+ }
+ }
+
+ // Check if backup objects are valid
+ // This is just a pre-check to avoid most of invalid backup requests.
+ // Also calculate the signature for incremental backup check.
+ List<TableRef> tblRefsNotSupport = Lists.newArrayList();
+ for (TableRef tableRef : tblRefs) {
+ String tblName = tableRef.getName().getTbl();
+ Table tbl = db.getTableOrDdlException(tblName);
+
+ // filter the table types which are not supported by local backup.
+ if (repository == null && tbl.getType() != TableType.OLAP
+ && tbl.getType() != TableType.VIEW && tbl.getType() !=
TableType.MATERIALIZED_VIEW) {
+ tblRefsNotSupport.add(tableRef);
+ continue;
+ }
+
+ if (tbl.getType() == TableType.VIEW || tbl.getType() ==
TableType.ODBC
+ || tbl.getType() == TableType.MATERIALIZED_VIEW) {
+ continue;
+ }
+ if (tbl.getType() != TableType.OLAP) {
+ if (Config.ignore_backup_not_support_table_type) {
+ LOG.warn("Table '{}' is a {} table, can not backup and
ignore it."
+ + "Only OLAP(Doris)/ODBC/VIEW table can be backed
up",
+ tblName, tbl.getType().toString());
+ tblRefsNotSupport.add(tableRef);
+ continue;
+ } else {
+
ErrorReport.reportDdlException(ErrorCode.ERR_NOT_OLAP_TABLE, tblName);
+ }
+ }
+
+ if (tbl.isTemporary()) {
+ if (Config.ignore_backup_not_support_table_type ||
tblRefs.size() > 1) {
+ LOG.warn("Table '{}' is a temporary table, can not backup
and ignore it."
+ + "Only OLAP(Doris)/ODBC/VIEW table can be backed
up",
+ Util.getTempTableDisplayName(tblName));
+ tblRefsNotSupport.add(tableRef);
+ continue;
+ } else {
+ ErrorReport.reportDdlException("Table " +
Util.getTempTableDisplayName(tblName)
+ + " is a temporary table, do not support backup");
+ }
+ }
+
+ OlapTable olapTbl = (OlapTable) tbl;
+ tbl.readLock();
+ try {
+ if (!Config.ignore_backup_tmp_partitions &&
olapTbl.existTempPartitions()) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "Do not support backup table " + olapTbl.getName()
+ " with temp partitions");
+ }
+
+ PartitionNames partitionNames = tableRef.getPartitionNames();
+ if (partitionNames != null) {
+ if (!Config.ignore_backup_tmp_partitions &&
partitionNames.isTemp()) {
+
ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "Do not support backup temp partitions in
table " + tableRef.getName());
+ }
+
+ for (String partName : partitionNames.getPartitionNames())
{
+ Partition partition = olapTbl.getPartition(partName);
+ if (partition == null) {
+
ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
+ "Unknown partition " + partName + " in
table" + tblName);
+ }
+ }
+ }
+ } finally {
+ tbl.readUnlock();
+ }
+ }
+
+ tblRefs.removeAll(tblRefsNotSupport);
+
+ // Check if label already be used
+ long repoId = Repository.KEEP_ON_LOCAL_REPO_ID;
+ if (repository != null) {
+ List<String> existSnapshotNames = Lists.newArrayList();
+ Status st = repository.listSnapshots(existSnapshotNames);
+ if (!st.ok()) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
st.getErrMsg());
+ }
+ if (existSnapshotNames.contains(command.getLabel())) {
+ if (command.getBackupType() == BackupCommand.BackupType.FULL) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
"Snapshot with name '"
+ + command.getLabel() + "' already exist in
repository");
+ } else {
+ ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
"Currently does not support "
+ + "incremental backup");
+ }
+ }
+ repoId = repository.getId();
+ }
+
+ // Create a backup job
+ BackupJob backupJob = new BackupJob(command.getLabel(), db.getId(),
+ ClusterNamespace.getNameFromFullName(db.getFullName()),
+ tblRefs, command.getTimeoutMs(),
command.translateToLagecyContent(), env, repoId, commitSeq);
+ // write log
+ env.getEditLog().logBackupJob(backupJob);
+
+ // must put to dbIdToBackupOrRestoreJob after edit log, otherwise the
state of job may be changed.
+ addBackupOrRestoreJob(db.getId(), backupJob);
+
+ LOG.info("finished to submit backup job: {}", backupJob);
+ }
+
private void backup(Repository repository, Database db, BackupStmt stmt)
throws DdlException {
if (repository != null && repository.isReadOnly()) {
ErrorReport.reportDdlException(ErrorCode.ERR_COMMON_ERROR,
"Repository " + repository.getName()
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 280aa909dd3..bdebfecaa38 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
@@ -592,6 +592,7 @@ import
org.apache.doris.nereids.trees.plans.commands.AlterWorkloadGroupCommand;
import
org.apache.doris.nereids.trees.plans.commands.AlterWorkloadPolicyCommand;
import org.apache.doris.nereids.trees.plans.commands.AnalyzeDatabaseCommand;
import org.apache.doris.nereids.trees.plans.commands.AnalyzeTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.BackupCommand;
import org.apache.doris.nereids.trees.plans.commands.CallCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelAlterTableCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelBackupCommand;
@@ -8177,6 +8178,30 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
return new RefreshDictionaryCommand(dbName, dictName);
}
+ @Override
+ public LogicalPlan visitBackup(DorisParser.BackupContext ctx) {
+ List<String> labelParts = visitMultipartIdentifier(ctx.label);
+ String snapshotName;
+ String dbName = null;
+ if (labelParts.size() == 1) {
+ snapshotName = labelParts.get(0);
+ } else if (labelParts.size() == 2) {
+ dbName = labelParts.get(0);
+ snapshotName = labelParts.get(1);
+ } else {
+ throw new ParseException("only support
[<db_name>.]<snapshot_name>", ctx.label);
+ }
+ LabelNameInfo labelNameInfo = new LabelNameInfo(dbName, snapshotName);
+ String repoName = ctx.repo.getText();
+ boolean isExclude = ctx.EXCLUDE() != null;
+ List<TableRefInfo> tableRefInfos = new ArrayList<>();
+ for (BaseTableRefContext baseTableRefContext : ctx.baseTableRef()) {
+ tableRefInfos.add(visitBaseTableRefContext(baseTableRefContext));
+ }
+ Map<String, String> properties = visitPropertyClause(ctx.properties);
+ return new BackupCommand(labelNameInfo, repoName, tableRefInfos,
properties, isExclude);
+ }
+
@Override
public LogicalPlan visitShowRoutineLoad(DorisParser.ShowRoutineLoadContext
ctx) {
LabelNameInfo labelNameInfo = null;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/TableSample.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/TableSample.java
index da300cd442d..b0949460fb0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/TableSample.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/TableSample.java
@@ -58,4 +58,8 @@ public class TableSample {
public int hashCode() {
return Objects.hash(sampleValue, isPercent, seek);
}
+
+ public org.apache.doris.analysis.TableSample
translateToLegacyTableSample() {
+ return new org.apache.doris.analysis.TableSample(this.isPercent,
this.sampleValue, this.seek);
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index 8762e4ecf6e..364f0aef223 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -402,6 +402,7 @@ public enum PlanType {
CANCEL_BACKUP_AND_RESTORE_COMMAND,
CANCEL_BUILD_INDEX_COMMAND,
CREATE_RESOURCE_COMMAND,
+ BACKUP_COMMAND,
CREATE_DATA_SYNC_JOB_COMMAND,
CREATE_STAGE_COMMAND,
DROP_STAGE_COMMAND,
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/BackupCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/BackupCommand.java
new file mode 100644
index 00000000000..7a275b1a716
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/BackupCommand.java
@@ -0,0 +1,274 @@
+// 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.BackupStmt;
+import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.commands.info.LabelNameInfo;
+import org.apache.doris.nereids.trees.plans.commands.info.TableRefInfo;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.GlobalVariable;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * BackupCommand
+ */
+public class BackupCommand extends Command implements ForwardWithSync {
+ private static final Logger LOG =
LogManager.getLogger(BackupCommand.class);
+ private static final String PROP_TIMEOUT = "timeout";
+ private static final long MIN_TIMEOUT_MS = 600 * 1000L;
+ private static final String PROP_TYPE = "type";
+ private static final String PROP_CONTENT = "content";
+
+ /**
+ * BackupType
+ */
+ public enum BackupType {
+ INCREMENTAL, FULL
+ }
+
+ /**
+ * BackupContent
+ */
+ public enum BackupContent {
+ METADATA_ONLY, ALL
+ }
+
+ private BackupType type = BackupType.FULL;
+ private BackupContent content = BackupContent.ALL;
+
+ private final LabelNameInfo labelNameInfo;
+ private final String repoName;
+ private final List<TableRefInfo> tableRefInfos;
+ private final Map<String, String> properties;
+ private final boolean isExclude;
+
+ private long timeoutMs;
+
+ /**
+ * BackupCommand
+ */
+ public BackupCommand(LabelNameInfo labelNameInfo,
+ String repoName,
+ List<TableRefInfo> tableRefInfos,
+ Map<String, String> properties,
+ boolean isExclude) {
+ super(PlanType.BACKUP_COMMAND);
+ Objects.requireNonNull(labelNameInfo, "labelNameInfo is null");
+ Objects.requireNonNull(repoName, "repoName is null");
+ Objects.requireNonNull(tableRefInfos, "tableRefInfos is null");
+ Objects.requireNonNull(properties, "properties is null");
+ this.labelNameInfo = labelNameInfo;
+ this.repoName = repoName;
+ this.tableRefInfos = tableRefInfos;
+ this.properties = properties;
+ this.isExclude = isExclude;
+ }
+
+ @Override
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate(ctx);
+ ctx.getEnv().getBackupHandler().process(this);
+ }
+
+ /**
+ * validate
+ */
+ public void validate(ConnectContext ctx) throws AnalysisException {
+ labelNameInfo.validate(ctx);
+
+ // user need database level privilege(not table level),
+ if (!Env.getCurrentEnv().getAccessManager()
+ .checkDbPriv(ConnectContext.get(),
InternalCatalog.INTERNAL_CATALOG_NAME,
+ labelNameInfo.getDb(), PrivPredicate.LOAD)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"LOAD");
+ }
+
+ analyzeTableRefInfo();
+ analyzeProperties();
+ }
+
+ private void analyzeTableRefInfo() throws AnalysisException {
+ if (tableRefInfos.isEmpty()) {
+ return;
+ }
+ checkTableRefWithoutDatabase();
+ updateTableRefInfos();
+ // tbl refs can not set alias in backup
+ for (TableRefInfo tableRefInfo : tableRefInfos) {
+ if (tableRefInfo.hasAlias()) {
+ throw new AnalysisException("Can not set alias for table in
Backup Command: " + tableRefInfo);
+ }
+ }
+ }
+
+ private void checkTableRefWithoutDatabase() throws AnalysisException {
+ for (TableRefInfo tableRef : tableRefInfos) {
+ if (!Strings.isNullOrEmpty(tableRef.getTableNameInfo().getDb())) {
+ throw new AnalysisException("Cannot specify database name on
backup objects: "
+ + tableRef.getTableNameInfo().getTbl() + ". Specify
database name before label");
+ }
+ // set db name because we can not persist empty string when
writing bdbje log
+ tableRef.getTableNameInfo().setDb(labelNameInfo.getDb());
+ }
+ }
+
+ private void updateTableRefInfos() throws AnalysisException {
+ Map<String, TableRefInfo> tblPartsMap;
+ if (GlobalVariable.lowerCaseTableNames == 0) {
+ // comparisons case sensitive
+ tblPartsMap = Maps.newTreeMap();
+ } else {
+ tblPartsMap = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ for (TableRefInfo tableRefInfo : tableRefInfos) {
+ String tableName = tableRefInfo.getTableNameInfo().getTbl();
+ if (!tblPartsMap.containsKey(tableName)) {
+ tblPartsMap.put(tableName, tableRefInfo);
+ } else {
+ throw new AnalysisException("Duplicated table: " + tableName);
+ }
+ }
+
+ // update table ref
+ tableRefInfos.clear();
+ tableRefInfos.addAll(tblPartsMap.values());
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("table refs after normalization: {}",
Joiner.on(",").join(tableRefInfos));
+ }
+ }
+
+ private void analyzeProperties() throws AnalysisException {
+ // timeout
+ if (properties.containsKey("timeout")) {
+ try {
+ timeoutMs = Long.valueOf(properties.get(PROP_TIMEOUT));
+ } catch (NumberFormatException e) {
+ ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR,
+ "Invalid timeout format: " +
properties.get(PROP_TIMEOUT));
+ }
+
+ if (timeoutMs * 1000 < MIN_TIMEOUT_MS) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR, "timeout must
be at least 10 min");
+ }
+
+ timeoutMs = timeoutMs * 1000;
+ properties.remove(PROP_TIMEOUT);
+ } else {
+ timeoutMs = Config.backup_job_default_timeout_ms;
+ }
+
+ Map<String, String> copiedProperties = Maps.newHashMap(properties);
+ // type
+ String typeProp = copiedProperties.get(PROP_TYPE);
+ if (typeProp != null) {
+ try {
+ type = BackupType.valueOf(typeProp.toUpperCase());
+ } catch (Exception e) {
+ ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR,
+ "Invalid backup job type: " + typeProp);
+ }
+ copiedProperties.remove(PROP_TYPE);
+ }
+ // content
+ String contentProp = copiedProperties.get(PROP_CONTENT);
+ if (contentProp != null) {
+ try {
+ content = BackupContent.valueOf(contentProp.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR,
+ "Invalid backup job content:" + contentProp);
+ }
+ copiedProperties.remove(PROP_CONTENT);
+ }
+
+ if (!copiedProperties.isEmpty()) {
+ ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR,
+ "Unknown backup job properties: " +
copiedProperties.keySet());
+ }
+ }
+
+ public List<TableRefInfo> getTableRefInfos() {
+ return tableRefInfos;
+ }
+
+ public long getTimeoutMs() {
+ return timeoutMs;
+ }
+
+ public boolean isExclude() {
+ return isExclude;
+ }
+
+ public String getRepoName() {
+ return repoName;
+ }
+
+ public BackupType getBackupType() {
+ return type;
+ }
+
+ public BackupStmt.BackupContent translateToLagecyContent() {
+ return BackupStmt.BackupContent.valueOf(content.name());
+ }
+
+ public String getLabel() {
+ return labelNameInfo.getLabel();
+ }
+
+ public String getDbName() {
+ return labelNameInfo.getDb();
+ }
+
+ @Override
+ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+ return visitor.visitBackupCommand(this, context);
+ }
+
+ @Override
+ public StmtType stmtType() {
+ return StmtType.BACKUP;
+ }
+
+ @Override
+ protected void checkSupportedInCloudMode(ConnectContext ctx) throws
DdlException {
+ LOG.info("BackupCommand not supported in cloud mode");
+ throw new DdlException("denied");
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/TableRefInfo.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/TableRefInfo.java
index 1a221b08f9e..0d3f142d86d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/TableRefInfo.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/TableRefInfo.java
@@ -18,12 +18,16 @@
package org.apache.doris.nereids.trees.plans.commands.info;
+import org.apache.doris.analysis.PartitionNames;
+import org.apache.doris.analysis.TableName;
+import org.apache.doris.analysis.TableRef;
import org.apache.doris.analysis.TableScanParams;
import org.apache.doris.analysis.TableSnapshot;
import org.apache.doris.common.UserException;
import org.apache.doris.nereids.trees.TableSample;
import org.apache.doris.qe.ConnectContext;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -75,4 +79,36 @@ public class TableRefInfo {
partitionNamesInfo.validate();
}
}
+
+ public String getTableAlias() {
+ return tableAlias;
+ }
+
+ public boolean hasAlias() {
+ return tableAlias != null;
+ }
+
+ /**
+ * translateToLegacyTableRef
+ */
+ public TableRef translateToLegacyTableRef() {
+ TableName tableName = new TableName(tableNameInfo.getCtl(),
tableNameInfo.getDb(), tableNameInfo.getTbl());
+ String alias = tableAlias;
+ PartitionNames partitionNames =
+ partitionNamesInfo != null ?
partitionNamesInfo.translateToLegacyPartitionNames() : null;
+ ArrayList<Long> sampleTabletIds = tabletIdList != null ? new
ArrayList<>(tabletIdList) : new ArrayList<>();
+ org.apache.doris.analysis.TableSample legacyTableSample =
+ tableSample != null ?
tableSample.translateToLegacyTableSample() : null;
+ ArrayList<String> commonHints = relationHints != null ? new
ArrayList<>(relationHints) : new ArrayList<>();
+ TableSnapshot tableSnapshot = tableSnapShot;
+ TableScanParams tableScanParams = scanParams;
+ return new TableRef(tableName,
+ alias,
+ partitionNames,
+ sampleTabletIds,
+ legacyTableSample,
+ commonHints,
+ tableSnapshot,
+ tableScanParams);
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
index ae852b9164d..a59b09ca4a5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
@@ -47,6 +47,7 @@ import
org.apache.doris.nereids.trees.plans.commands.AlterUserCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterViewCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterWorkloadGroupCommand;
import
org.apache.doris.nereids.trees.plans.commands.AlterWorkloadPolicyCommand;
+import org.apache.doris.nereids.trees.plans.commands.BackupCommand;
import org.apache.doris.nereids.trees.plans.commands.CallCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelAlterTableCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelBackupCommand;
@@ -1240,6 +1241,10 @@ public interface CommandVisitor<R, C> {
return visitCommand(createResourceCommand, context);
}
+ default R visitBackupCommand(BackupCommand backupCommand, C context) {
+ return visitCommand(backupCommand, context);
+ }
+
default R visitRefreshLdapCommand(RefreshLdapCommand command, C context) {
return visitCommand(command, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/BackupCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/BackupCommandTest.java
new file mode 100644
index 00000000000..7510e2468e1
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/BackupCommandTest.java
@@ -0,0 +1,125 @@
+// 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.catalog.Env;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.commands.info.LabelNameInfo;
+import org.apache.doris.nereids.trees.plans.commands.info.PartitionNamesInfo;
+import org.apache.doris.nereids.trees.plans.commands.info.TableNameInfo;
+import org.apache.doris.nereids.trees.plans.commands.info.TableRefInfo;
+import org.apache.doris.qe.ConnectContext;
+
+import mockit.Expectations;
+import mockit.Mocked;
+import org.apache.commons.collections.map.HashedMap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class BackupCommandTest {
+ private static final String internalCtl =
InternalCatalog.INTERNAL_CATALOG_NAME;
+ @Mocked
+ private Env env;
+ @Mocked
+ private AccessControllerManager accessControllerManager;
+ @Mocked
+ private ConnectContext connectContext;
+
+ private String dbName = "test_db";
+
+ private void runBefore() {
+ new Expectations() {
+ {
+ Env.getCurrentEnv();
+ minTimes = 0;
+ result = env;
+
+ env.getAccessManager();
+ minTimes = 0;
+ result = accessControllerManager;
+
+ ConnectContext.get();
+ minTimes = 0;
+ result = connectContext;
+
+ connectContext.isSkipAuth();
+ minTimes = 0;
+ result = true;
+
+ accessControllerManager.checkDbPriv(connectContext,
internalCtl, dbName, PrivPredicate.LOAD);
+ minTimes = 0;
+ result = true;
+ }
+ };
+ }
+
+ @Test
+ public void testValidateNormal() {
+ runBefore();
+ LabelNameInfo labelNameInfo = new LabelNameInfo(dbName, "label0");
+ String repoName = "testRepo";
+
+ TableNameInfo tableNameInfo = new TableNameInfo(internalCtl, null,
"test_tbl");
+ List<String> partitionNames = new ArrayList<>();
+ partitionNames.add("p1");
+ partitionNames.add("p2");
+ PartitionNamesInfo partitionNamesInfo = new PartitionNamesInfo(false,
partitionNames);
+ TableRefInfo tableRefInfo1 = new TableRefInfo(tableNameInfo, null,
null, partitionNamesInfo, null, null, null, null);
+
+ TableNameInfo tableNameInfo2 = new TableNameInfo(internalCtl, null,
"test_tbl2");
+ TableRefInfo tableRefInfo2 = new TableRefInfo(tableNameInfo2, null,
null, partitionNamesInfo, null, null, null, null);
+
+ List<TableRefInfo> tableRefInfos = new ArrayList<>();
+ tableRefInfos.add(tableRefInfo1);
+ tableRefInfos.add(tableRefInfo2);
+
+ Map<String, String> properties = new HashedMap();
+ properties.put("timeout", "86400");
+ properties.put("type", BackupCommand.BackupType.FULL.name());
+ properties.put("content", BackupCommand.BackupContent.ALL.name());
+
+ boolean isExclude = false;
+
+ BackupCommand command = new BackupCommand(labelNameInfo, repoName,
tableRefInfos, properties, isExclude);
+ Assertions.assertDoesNotThrow(() -> command.validate(connectContext));
+
+ List<TableRefInfo> tableRefInfos2 = new ArrayList<>();
+ BackupCommand command2 = new BackupCommand(labelNameInfo, repoName,
tableRefInfos2, properties, isExclude);
+ Assertions.assertDoesNotThrow(() -> command2.validate(connectContext));
+
+ TableNameInfo tableNameInfo3 = new TableNameInfo(internalCtl, null,
"test_tbl");
+ TableRefInfo tableRefInfo3 = new TableRefInfo(tableNameInfo3, null,
null, partitionNamesInfo, null, null, null, null);
+ List<TableRefInfo> tableRefInfos3 = new ArrayList<>();
+ tableRefInfos3.add(tableRefInfo1);
+ tableRefInfos.add(tableRefInfo3);
+ BackupCommand command3 = new BackupCommand(labelNameInfo, repoName,
tableRefInfos3, properties, isExclude);
+ Assertions.assertThrows(AnalysisException.class, () ->
command3.validate(connectContext));
+
+ Map<String, String> properties2 = new HashedMap();
+ properties.put("key1", "value1");
+ BackupCommand command4 = new BackupCommand(labelNameInfo, repoName,
tableRefInfos, properties2, isExclude);
+ Assertions.assertThrows(AnalysisException.class, () ->
command4.validate(connectContext));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]