Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb5a55b
[feature](fe) Add routine load target-table alter support
0AyanamiRei Jun 26, 2026
03cf626
[fix](fe) Tighten routine load alter validation
0AyanamiRei Jun 26, 2026
a8bd6be
Merge branch 'master' into feature/routine-load-target-table-switch
0AyanamiRei Jun 29, 2026
9b2cf36
[fix](fe) Fix routine load alter checkstyle issues
0AyanamiRei Jun 29, 2026
760890d
[fix](fe) Refine routine load alter target-table follow-up
0AyanamiRei Jun 29, 2026
931b717
[fix](fe) Align routine load alter partial update mode
0AyanamiRei Jun 29, 2026
48f1b8c
[fix](fe) Fix routine load alter checkstyle line length
0AyanamiRei Jun 29, 2026
0f1be9e
[fix](regression) Fix routine load alter test output checks
0AyanamiRei Jun 29, 2026
4a34f9f
[feature](routine-load) Support composable target table alteration
0AyanamiRei Jul 15, 2026
000c838
[fix](fe) Fix routine load alter review issues
0AyanamiRei Jul 16, 2026
cd0cd0c
Merge branch 'master' into feature/routine-load-target-table-switch
0AyanamiRei Jul 16, 2026
cfb7382
[fix](fe) Restrict routine load target alteration to Kafka
0AyanamiRei Jul 16, 2026
bd1fad5
[fix](fe) Restore Kinesis routine load alter hook
0AyanamiRei Jul 16, 2026
77cb200
Merge branch 'master' into feature/routine-load-target-table-switch
0AyanamiRei Jul 16, 2026
e848c18
[refactor](fe) Simplify routine load target table alteration
0AyanamiRei Jul 17, 2026
8b19f60
[fix](fe) Move routine load ALTER I/O outside metadata locks
0AyanamiRei Jul 17, 2026
e93f300
[fix](fe) Make routine load ALTER state durable
0AyanamiRei Jul 17, 2026
0f81dc7
[improvement](fe) Reduce routine load ALTER lock scope
0AyanamiRei Jul 17, 2026
ba918d0
[fix](fe) Authorize routine load target before metadata lookup
0AyanamiRei Jul 19, 2026
a756d7c
[refactor](fe) Narrow routine load target-table changes
0AyanamiRei Jul 19, 2026
0fca4e7
[test](routineload) Improve target-table alter coverage
0AyanamiRei Jul 23, 2026
04a5e10
[chore](routineload) Resolve master merge conflict
0AyanamiRei Jul 23, 2026
963ed3e
[fix](routineload) Restrict target-table alter to Kafka
0AyanamiRei Jul 23, 2026
66a665c
[fix](routineload) Validate target table with altered job properties
0AyanamiRei Jul 27, 2026
e589ea0
Merge branch 'master' into feature/routine-load-target-table-switch
0AyanamiRei Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.apache.doris.analysis.Expr;
import org.apache.doris.analysis.ExprToSqlVisitor;
import org.apache.doris.analysis.ImportColumnDesc;
import org.apache.doris.analysis.Separator;
import org.apache.doris.analysis.ToSqlParams;
import org.apache.doris.analysis.UserIdentity;
Expand Down Expand Up @@ -470,6 +471,44 @@ protected void setRoutineLoadDesc(RoutineLoadDesc routineLoadDesc) {
}
}

public void validateTargetTable(Database db, OlapTable targetTable,
Map<String, String> alteredJobProperties) throws UserException {
if (isMultiTable) {
throw new AnalysisException("ALTER ROUTINE LOAD target table change only supports single-table job");
}
List<ImportColumnDesc> columnsInfo = null;
if (columnDescs != null && !columnDescs.descs.isEmpty()) {
columnsInfo = new ArrayList<>(columnDescs.descs);
}
checkMeta(targetTable, new RoutineLoadDesc(columnSeparator, lineDelimiter, columnsInfo,
precedingFilter, whereExpr, partitionNamesInfo, deleteCondition, mergeType, sequenceCol));

targetTable.readLock();
try {
NereidsRoutineLoadTaskInfo taskInfo = toNereidsRoutineLoadTaskInfo();
taskInfo.applyAlterPropertiesForValidation(
alteredJobProperties, getEffectiveUniqueKeyUpdateMode(alteredJobProperties));
NereidsStreamLoadPlanner planner = new NereidsStreamLoadPlanner(db, targetTable, taskInfo);
planner.plan(new TUniqueId(0, 0));
} finally {
targetTable.readUnlock();
}
}

private TUniqueKeyUpdateMode getEffectiveUniqueKeyUpdateMode(Map<String, String> alteredJobProperties) {
TUniqueKeyUpdateMode effectiveUniqueKeyUpdateMode = uniqueKeyUpdateMode;
if (alteredJobProperties.containsKey(CreateRoutineLoadInfo.UNIQUE_KEY_UPDATE_MODE)) {
effectiveUniqueKeyUpdateMode = TUniqueKeyUpdateMode.valueOf(
alteredJobProperties.get(CreateRoutineLoadInfo.UNIQUE_KEY_UPDATE_MODE));
}
if (alteredJobProperties.containsKey(CreateRoutineLoadInfo.PARTIAL_COLUMNS)
&& Boolean.parseBoolean(alteredJobProperties.get(CreateRoutineLoadInfo.PARTIAL_COLUMNS))
&& effectiveUniqueKeyUpdateMode == TUniqueKeyUpdateMode.UPSERT) {
effectiveUniqueKeyUpdateMode = TUniqueKeyUpdateMode.UPDATE_FIXED_COLUMNS;
}
return effectiveUniqueKeyUpdateMode;
}

@Override
public long getId() {
return id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,9 +782,12 @@ public void modifyProperties(AlterRoutineLoadCommand command) throws UserExcepti
}

modifyPropertiesInternal(jobProperties, dataSourceProperties);
Comment thread
0AyanamiRei marked this conversation as resolved.
if (command.hasTargetTable()) {
tableId = command.getTargetTableId();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Re-seed progress when a broker-list change selects a different Kafka cluster. With the topic unchanged, modifyPropertiesInternal() only replaces brokerList; it retains progress, custom/current partitions, and the latest-offset cache. Pinned jobs then skip metadata discovery, and a dynamic job whose new cluster has the same numeric partition IDs appears unchanged, so tasks combine the new brokers and target here with offsets from the old cluster. Switching cluster A at offset 1000 to cluster B can therefore skip B's first 1000 records or pause with OFFSET_OUT_OF_RANGE. Resolve the cluster ID and any requested offsets against the effective new brokers outside the job lock; if the cluster changed, require those validated offsets or reset/reseed all progress and partition caches, persist that prepared decision for replay, and recheck the job generation before committing.

Comment thread
0AyanamiRei marked this conversation as resolved.
Comment thread
0AyanamiRei marked this conversation as resolved.
}

AlterRoutineLoadJobOperationLog log = new AlterRoutineLoadJobOperationLog(this.id,
jobProperties, dataSourceProperties);
jobProperties, dataSourceProperties, command.getTargetTableId());
Env.getCurrentEnv().getEditLog().logAlterRoutineLoadJob(log);
} finally {
writeUnlock();
Expand Down Expand Up @@ -908,6 +911,9 @@ private void resetCloudProgress(Cloud.ResetRLProgressRequest.Builder builder) th
public void replayModifyProperties(AlterRoutineLoadJobOperationLog log) {
try {
modifyPropertiesInternal(log.getJobProperties(), (KafkaDataSourceProperties) log.getDataSourceProperties());
if (log.getTargetTableId() != 0) {
Comment thread
0AyanamiRei marked this conversation as resolved.
tableId = log.getTargetTableId();
}
} catch (UserException e) {
// should not happen
LOG.error("failed to replay modify kafka routine load job: {}", id, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ public int calTimeoutSec() {
return realTimeoutSec;
}

/** Apply analyzed ALTER properties to this validation-only task. */
public void applyAlterPropertiesForValidation(Map<String, String> alteredJobProperties,
TUniqueKeyUpdateMode effectiveUniqueKeyUpdateMode) {
jobProperties.putAll(alteredJobProperties);
uniquekeyUpdateMode = effectiveUniqueKeyUpdateMode;
}

@Override
public int getTimeout() {
return this.timeoutSec;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9361,6 +9361,8 @@ public LogicalPlan visitAlterRoutineLoad(DorisParser.AlterRoutineLoadContext ctx
throw new ParseException("only support [<db>.]<job_name>", ctx.name);
}
LabelNameInfo labelNameInfo = new LabelNameInfo(dbName, jobName);
String targetTableName = ctx.targetTable == null
? null : SqlLiteralUtils.parseStringLiteral(ctx.targetTable.getText());

Map<String, String> properties = new HashMap<>();
if (ctx.properties != null) {
Expand All @@ -9387,7 +9389,8 @@ public LogicalPlan visitAlterRoutineLoad(DorisParser.AlterRoutineLoadContext ctx
}
}

return new AlterRoutineLoadCommand(labelNameInfo, loadPropertyMap, properties, dataSourceMapProperties);
return new AlterRoutineLoadCommand(labelNameInfo, targetTableName,
Comment thread
0AyanamiRei marked this conversation as resolved.
loadPropertyMap, properties, dataSourceMapProperties);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,24 @@
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.RandomDistributionInfo;
import org.apache.doris.catalog.Table;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.datasource.property.fileformat.CsvFileFormatProperties;
import org.apache.doris.datasource.property.fileformat.JsonFileFormatProperties;
import org.apache.doris.load.RoutineLoadDesc;
import org.apache.doris.load.routineload.AbstractDataSourceProperties;
import org.apache.doris.load.routineload.LoadDataSourceType;
import org.apache.doris.load.routineload.RoutineLoadDataSourcePropertyFactory;
import org.apache.doris.load.routineload.RoutineLoadJob;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.CreateRoutineLoadInfo;
import org.apache.doris.nereids.trees.plans.commands.info.LabelNameInfo;
Expand All @@ -52,6 +58,7 @@

/**
* ALTER ROUTINE LOAD db.label
* SET TARGET TABLE = "table"
* PROPERTIES(
* ...
* )
Expand Down Expand Up @@ -85,10 +92,12 @@ public class AlterRoutineLoadCommand extends AlterCommand {
.build();

private final LabelNameInfo labelNameInfo;
private final String targetTableName;
private final Map<String, LoadProperty> loadPropertyMap;
private RoutineLoadDesc routineLoadDesc;
private final Map<String, String> jobProperties;
private final Map<String, String> dataSourceMapProperties;
private long targetTableId;
private boolean isPartialUpdate;

// save analyzed job properties.
Expand All @@ -100,6 +109,7 @@ public class AlterRoutineLoadCommand extends AlterCommand {
* AlterRoutineLoadCommand
*/
public AlterRoutineLoadCommand(LabelNameInfo labelNameInfo,
String targetTableName,
Map<String, LoadProperty> loadPropertyMap,
Map<String, String> jobProperties,
Map<String, String> dataSourceMapProperties) {
Expand All @@ -108,6 +118,7 @@ public AlterRoutineLoadCommand(LabelNameInfo labelNameInfo,
Objects.requireNonNull(jobProperties, "jobProperties is null");
Objects.requireNonNull(dataSourceMapProperties, "dataSourceMapProperties is null");
this.labelNameInfo = labelNameInfo;
this.targetTableName = targetTableName;
this.loadPropertyMap = loadPropertyMap == null ? Maps.newHashMap() : loadPropertyMap;
this.jobProperties = jobProperties;
this.dataSourceMapProperties = dataSourceMapProperties;
Expand All @@ -118,7 +129,7 @@ public AlterRoutineLoadCommand(LabelNameInfo labelNameInfo,
public AlterRoutineLoadCommand(LabelNameInfo labelNameInfo,
Map<String, String> jobProperties,
Map<String, String> dataSourceMapProperties) {
this(labelNameInfo, Maps.newHashMap(), jobProperties, dataSourceMapProperties);
this(labelNameInfo, null, Maps.newHashMap(), jobProperties, dataSourceMapProperties);
}

public String getDbName() {
Expand All @@ -133,6 +144,18 @@ public Map<String, String> getAnalyzedJobProperties() {
return analyzedJobProperties;
}

public boolean hasTargetTable() {
return targetTableName != null;
}

public String getTargetTableName() {
return targetTableName;
}

public long getTargetTableId() {
return targetTableId;
}

public boolean hasDataSourceProperty() {
return MapUtils.isNotEmpty(dataSourceMapProperties);
}
Expand Down Expand Up @@ -164,15 +187,22 @@ public void validate(ConnectContext ctx) throws UserException {
// check routine load job properties include desired concurrent number etc.
checkJobProperties();
// check load properties
RoutineLoadJob job = Env.getCurrentEnv().getRoutineLoadManager()
.getJob(getDbName(), getJobName());
this.routineLoadDesc = CreateRoutineLoadInfo.checkLoadProperties(ctx, loadPropertyMap,
job.getDbFullName(), job.getTableName(), job.isMultiTable(), job.getMergeType());
RoutineLoadJob job = hasTargetTable()
? Env.getCurrentEnv().getRoutineLoadManager().checkPrivAndGetJob(getDbName(), getJobName())
: Env.getCurrentEnv().getRoutineLoadManager().getJob(getDbName(), getJobName());
if (MapUtils.isNotEmpty(loadPropertyMap)) {
this.routineLoadDesc = CreateRoutineLoadInfo.checkLoadProperties(ctx, loadPropertyMap,
job.getDbFullName(), job.getTableName(), job.isMultiTable(), job.getMergeType());
Comment thread
0AyanamiRei marked this conversation as resolved.
}
// check data source properties
checkDataSourceProperties();
checkPartialUpdate();
if (hasTargetTable()) {
validateTargetTable(ctx, job);
Comment thread
0AyanamiRei marked this conversation as resolved.
Comment thread
0AyanamiRei marked this conversation as resolved.
} else {
checkPartialUpdate(job);
}
if (analyzedJobProperties.isEmpty() && MapUtils.isEmpty(dataSourceMapProperties)
&& routineLoadDesc == null) {
&& routineLoadDesc == null && !hasTargetTable()) {
throw new AnalysisException("No properties are specified");
}
}
Expand Down Expand Up @@ -328,12 +358,10 @@ private void checkDataSourceProperties() throws UserException {
dataSourceProperties.analyze();
}

private void checkPartialUpdate() throws UserException {
private void checkPartialUpdate(RoutineLoadJob job) throws UserException {
if (!isPartialUpdate) {
return;
}
RoutineLoadJob job = Env.getCurrentEnv().getRoutineLoadManager()
.getJob(getDbName(), getDbName());
if (job.isMultiTable()) {
throw new AnalysisException("load by PARTIAL_COLUMNS is not supported in multi-table load.");
}
Expand All @@ -344,6 +372,37 @@ private void checkPartialUpdate() throws UserException {
}
}

private void validateTargetTable(ConnectContext ctx, RoutineLoadJob job) throws UserException {
if (job.getDataSourceType() != LoadDataSourceType.KAFKA) {
throw new AnalysisException("ALTER ROUTINE LOAD target table change only supports Kafka jobs");
}
if (job.isMultiTable()) {
throw new AnalysisException("ALTER ROUTINE LOAD target table change only supports single-table job");
}
String dbFullName = job.getDbFullName();
if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, InternalCatalog.INTERNAL_CATALOG_NAME,
dbFullName, targetTableName, PrivPredicate.LOAD)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, "LOAD",
ctx.getQualifiedUser(), ctx.getRemoteIP(), dbFullName + ": " + targetTableName);
}
Database db = Env.getCurrentInternalCatalog().getDbOrAnalysisException(dbFullName);
Table table = db.getTableOrAnalysisException(targetTableName);
Comment thread
0AyanamiRei marked this conversation as resolved.
if (!(table instanceof OlapTable)) {
throw new AnalysisException("ALTER ROUTINE LOAD target table only supports OLAP table");
}
OlapTable olapTable = (OlapTable) table;
if (olapTable.isTemporary()) {
throw new AnalysisException("Do not support load for temporary table " + olapTable.getDisplayName());
}
if (job.isLoadToSingleTablet()
&& !(olapTable.getDefaultDistributionInfo() instanceof RandomDistributionInfo)) {
throw new AnalysisException(
"if load_to_single_tablet set to true, the olap table must be with random distribution");
}
job.validateTargetTable(db, olapTable, analyzedJobProperties);
this.targetTableId = olapTable.getId();
}

@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitAlterRoutineLoadCommand(this, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ public class AlterRoutineLoadJobOperationLog implements Writable {
private Map<String, String> jobProperties;
@SerializedName(value = "dataSourceProperties")
private AbstractDataSourceProperties dataSourceProperties;
@SerializedName(value = "targetTableId")
private long targetTableId;
Comment thread
sollhui marked this conversation as resolved.

public AlterRoutineLoadJobOperationLog(long jobId, Map<String, String> jobProperties,
AbstractDataSourceProperties dataSourceProperties) {
this(jobId, jobProperties, dataSourceProperties, 0L);
}

public AlterRoutineLoadJobOperationLog(long jobId, Map<String, String> jobProperties,
AbstractDataSourceProperties dataSourceProperties, long targetTableId) {
this.jobId = jobId;
this.jobProperties = jobProperties;
this.dataSourceProperties = dataSourceProperties;
this.targetTableId = targetTableId;
}

public long getJobId() {
Expand All @@ -57,6 +65,10 @@ public AbstractDataSourceProperties getDataSourceProperties() {
return dataSourceProperties;
}

public long getTargetTableId() {
return targetTableId;
}

public static AlterRoutineLoadJobOperationLog read(DataInput in) throws IOException {
String json = Text.readString(in);
return GsonUtils.GSON.fromJson(json, AlterRoutineLoadJobOperationLog.class);
Expand Down
Loading
Loading