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

morningman 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 3cf8cc628fb [fix](fe) Make SHOW CREATE ROUTINE LOAD output recreate 
the same job (#70118)
3cf8cc628fb is described below

commit 3cf8cc628fbe2c1427ddaa94fcf29e8f40433172
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Nov 5 07:18:24 2026 +0000

    [fix](fe) Make SHOW CREATE ROUTINE LOAD output recreate the same job 
(#70118)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #69749
    
    Problem Summary:
    
    **In short.** `SHOW CREATE ROUTINE LOAD` prints the statement a job is
    recreated from, but a job recreated from it was not the same job: eight
    properties that CREATE accepts were never printed, a value with a quote
    or a backslash made the statement unparseable, `TEMPORARY` partitions
    turned into formal ones, and the comment was dropped. This PR makes the
    statement carry all of them.
    
    **Background.** Users recreate a routine load job from `SHOW CREATE
    ROUTINE LOAD` to rename it, to move it to another database, or to start
    it over after `STOP`; the statement prints the next Kafka offsets to
    consume for exactly that purpose. #69749 made the statement carry the
    `compute_group` a job declares, and found the rest of the list
    incomplete: `RoutineLoadJob.getShowCreateInfo()` writes the properties
    by hand and covered 17 of the 25 keys in
    `CreateRoutineLoadInfo.PROPERTIES_SET`.
    
    **The problem, and what it cost.** Measured on master `1aa505cb0be` with
    a unit test that creates jobs from SQL, recreates each one from its
    `SHOW CREATE` output and compares every property:
    
    | Job | Recreated from its SHOW CREATE output |
    |---|---|
    | CSV job with `enclose` `"`, `escape` `\`, `empty_field_as_null`,
    `send_batch_parallelism` = 3, `load_to_single_tablet`, `workload_group`
    | all six back to their defaults; quoted fields get split at the
    separators inside the quotes |
    | `unique_key_update_mode` = `UPDATE_FLEXIBLE_COLUMNS` with
    `partial_update_new_key_behavior` = `ERROR` | an `UPSERT` job with
    `APPEND` |
    | partial columns with `partial_update_new_key_behavior` = `ERROR` |
    `APPEND`: rows with new keys get inserted instead of failing the load |
    | JSON job with `jsonpaths` such as `["$.k1","$.v1"]` | the statement
    does not parse (`mismatched input '$'`) |
    | Kafka property whose value holds a `"`, such as a JAAS config | the
    statement does not parse |
    | `TEMPORARY PARTITION(tp1)` | printed as `PARTITION(tp1)`: `Partition
    tp1 does not exist` |
    | `COMMENT '...'` | gone |
    
    The jobs themselves keep running as declared; only the statement is
    wrong. The cost is a recreated job that silently loads differently (CSV
    parsing, partial update semantics, single-tablet loading, workload
    group, parallelism), or a statement that cannot be executed at all.
    
    **How this PR fixes it.**
    
    - `getShowCreateInfo()` prints the eight missing properties wherever
    leaving one out would give the recreated job a different value:
    `unique_key_update_mode` when it is not `UPSERT` (`partial_columns`
    alone cannot express `UPDATE_FLEXIBLE_COLUMNS`),
    `partial_update_new_key_behavior` when it is `ERROR` and a partial
    update mode is on (CREATE rejects the policy otherwise), `enclose` and
    `escape` when set, `empty_field_as_null` and `load_to_single_tablet`
    when true, and `workload_group` when set. `send_batch_parallelism` is
    always printed, as the timezone is, because CREATE otherwise takes it
    from the session variable of whoever recreates the job. The values are
    the ones the job runs with.
    - Keys and values are quoted with `SqlLiteralUtils.quoteStringLiteral`,
    which follows the session's `NO_BACKSLASH_ESCAPES` mode. A value without
    a quote or a backslash prints exactly as before; the column separator is
    left as it is, since it is kept as typed and already reads back.
    - `TEMPORARY PARTITION(...)` keeps `TEMPORARY`, and the job's comment is
    printed as `COMMENT "..."`.
    - `CREATE ROUTINE LOAD` reads its `COMMENT` with
    `SqlLiteralUtils.parseStringLiteral`, as table, view and column comments
    are read, so that a printed comment with a quote reads back unchanged.
    It used to strip the outer quotes only, so `COMMENT 'it''s'` was stored
    as `it''s`.
    
    For a job that uses none of these, the output gains a single line,
    `"send_batch_parallelism" = "<the job's value>"`.
    
    **Results.**
    
    | Job | Before | After |
    |---|---|---|
    | CSV job above | 6 properties and the comment lost | recreated with the
    same definition |
    | flexible partial update with `ERROR` | `UPSERT` / `APPEND` | same
    definition |
    | JSON job with `jsonpaths` | statement does not parse | same definition
    |
    | Kafka property with `"` and `\` | statement does not parse | same
    definition |
    | temporary partition | `Partition tp1 does not exist` | same definition
    |
    | `NO_BACKSLASH_ESCAPES` session, `enclose` `"`, `escape` `\` | both
    lost | same definition |
    
    The same test also found that a job's `max_filter_ratio`,
    `exec_mem_limit`, `send_batch_parallelism`, `load_to_single_tablet`,
    `enclose` and `escape` fall back to their defaults when a follower
    replays the job or the FE restarts, and that `ALTER ROUTINE LOAD` on
    `enclose` / `escape` does not reach the running job. That is a separate
    defect and will be fixed in its own PR; since this PR prints the values
    the job runs with, the statement follows that fix. branch-4.1 and
    branch-4.2 have the same gaps as master.
---
 .../doris/load/routineload/RoutineLoadJob.java     |  42 +++-
 .../doris/nereids/parser/LogicalPlanBuilder.java   |   5 +-
 .../doris/load/routineload/RoutineLoadJobTest.java |  53 ++++
 .../RoutineLoadShowCreateRoundTripTest.java        | 268 +++++++++++++++++++++
 4 files changed, 363 insertions(+), 5 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadJob.java
 
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadJob.java
index 6c644eb4792..68f1c320d58 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadJob.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadJob.java
@@ -58,6 +58,7 @@ import 
org.apache.doris.nereids.trees.plans.commands.AlterRoutineLoadCommand;
 import org.apache.doris.nereids.trees.plans.commands.LoadCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.info.CreateRoutineLoadInfo;
 import 
org.apache.doris.nereids.trees.plans.commands.load.CreateRoutineLoadCommand;
+import org.apache.doris.nereids.util.SqlLiteralUtils;
 import org.apache.doris.persist.AlterRoutineLoadJobOperationLog;
 import org.apache.doris.persist.RoutineLoadOperation;
 import org.apache.doris.persist.gson.GsonPostProcessable;
@@ -1824,7 +1825,8 @@ public abstract class RoutineLoadJob
         }
         // 4.4.partitions
         if (partitionNamesInfo != null) {
-            
sb.append("PARTITION(").append(Joiner.on(",").join(partitionNamesInfo.getPartitionNames())).append("),\n");
+            sb.append(partitionNamesInfo.isTemp() ? "TEMPORARY PARTITION(" : 
"PARTITION(")
+                    
.append(Joiner.on(",").join(partitionNamesInfo.getPartitionNames())).append("),\n");
         }
         // 4.5.delete_on_predicates
         if (deleteCondition != null) {
@@ -1844,7 +1846,7 @@ public abstract class RoutineLoadJob
         if (sb.charAt(sb.length() - 2) == ',') {
             sb.replace(sb.length() - 2, sb.length() - 1, "");
         }
-        // 5.job_properties. See PROPERTIES_SET of CreateRoutineLoadStmt
+        // 5.job_properties. See PROPERTIES_SET of CreateRoutineLoadInfo
         sb.append("PROPERTIES\n(\n");
         appendProperties(sb, 
CreateRoutineLoadInfo.DESIRED_CONCURRENT_NUMBER_PROPERTY, 
desireTaskConcurrentNum, false);
         appendProperties(sb, CreateRoutineLoadInfo.MAX_ERROR_NUMBER_PROPERTY, 
maxErrorNum, false);
@@ -1856,13 +1858,37 @@ public abstract class RoutineLoadJob
         if (isPartialUpdate) {
             appendProperties(sb, CreateRoutineLoadInfo.PARTIAL_COLUMNS, 
isPartialUpdate, false);
         }
+        // UPDATE_FLEXIBLE_COLUMNS leaves isPartialUpdate false, so without 
the mode the job would be recreated
+        // as an UPSERT one. CREATE only accepts the new key policy together 
with a partial update mode.
+        if (uniqueKeyUpdateMode != TUniqueKeyUpdateMode.UPSERT) {
+            appendProperties(sb, CreateRoutineLoadInfo.UNIQUE_KEY_UPDATE_MODE, 
uniqueKeyUpdateMode.name(), false);
+            if (partialUpdateNewKeyPolicy == TPartialUpdateNewRowPolicy.ERROR) 
{
+                appendProperties(sb, 
CreateRoutineLoadInfo.PARTIAL_UPDATE_NEW_KEY_POLICY, "ERROR", false);
+            }
+        }
         appendProperties(sb, JsonFileFormatProperties.PROP_JSON_PATHS, 
getJsonPaths(), false);
         appendProperties(sb, JsonFileFormatProperties.PROP_STRIP_OUTER_ARRAY, 
isStripOuterArray(), false);
         appendProperties(sb, JsonFileFormatProperties.PROP_NUM_AS_STRING, 
isNumAsString(), false);
         appendProperties(sb, JsonFileFormatProperties.PROP_FUZZY_PARSE, 
isFuzzyParse(), false);
         appendProperties(sb, JsonFileFormatProperties.PROP_JSON_ROOT, 
getJsonRoot(), false);
+        if (getEnclose() != 0) {
+            appendProperties(sb, CsvFileFormatProperties.PROP_ENCLOSE, new 
String(new byte[]{getEnclose()}), false);
+        }
+        if (getEscape() != 0) {
+            appendProperties(sb, CsvFileFormatProperties.PROP_ESCAPE, new 
String(new byte[]{getEscape()}), false);
+        }
+        if (getEmptyFieldAsNull()) {
+            appendProperties(sb, 
CsvFileFormatProperties.PROP_EMPTY_FIELD_AS_NULL, true, false);
+        }
         appendProperties(sb, LoadCommand.STRICT_MODE, isStrictMode(), false);
         appendProperties(sb, LoadCommand.TIMEZONE, getTimezone(), false);
+        // Always written, as the timezone is: left out, CREATE takes it from 
the session variable of whoever
+        // recreates the job.
+        appendProperties(sb, CreateRoutineLoadInfo.SEND_BATCH_PARALLELISM, 
getSendBatchParallelism(), false);
+        if (isLoadToSingleTablet()) {
+            appendProperties(sb, CreateRoutineLoadInfo.LOAD_TO_SINGLE_TABLET, 
true, false);
+        }
+        appendProperties(sb, CreateRoutineLoadInfo.WORKLOAD_GROUP, 
getWorkloadGroup(), false);
         // Only a declared group, which follows ALTER. Writing the effective 
group instead would turn
         // a group inherited from the creating session into a pinned one when 
the statement is replayed.
         appendProperties(sb, CreateRoutineLoadInfo.COMPUTE_GROUP, 
getDeclaredComputeGroup(), false);
@@ -1885,7 +1911,12 @@ public abstract class RoutineLoadJob
         }
         // remove the last ","
         sb.replace(sb.length() - 2, sb.length() - 1, "");
-        sb.append(");");
+        sb.append(")");
+        // 8. comment
+        if (!Strings.isNullOrEmpty(comment)) {
+            sb.append("\nCOMMENT 
").append(SqlLiteralUtils.quoteStringLiteral(comment));
+        }
+        sb.append(";");
         return sb.toString();
     }
 
@@ -1893,7 +1924,10 @@ public abstract class RoutineLoadJob
         if (value == null || Strings.isNullOrEmpty(value.toString())) {
             return;
         }
-        sb.append("\"").append(key).append("\"").append(" = 
").append("\"").append(value).append("\"");
+        // Quoted the way the parser reads string literals back: a jsonpaths 
value such as ["$.k1"] or an
+        // enclose of " would otherwise end the literal early, and an escape 
of \ would escape the closing quote.
+        sb.append(SqlLiteralUtils.quoteStringLiteral(key)).append(" = ")
+                .append(SqlLiteralUtils.quoteStringLiteral(value.toString()));
         if (!end) {
             sb.append(",\n");
         } else {
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 1a3624a3527..2a11873e7cc 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
@@ -2576,7 +2576,10 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 mergeType = LoadTask.MergeType.MERGE;
             }
         }
-        String comment = visitCommentSpec(ctx.commentSpec());
+        // Read as a string literal, as column comments are, so that the 
COMMENT that SHOW CREATE ROUTINE LOAD
+        // prints reads back unchanged
+        String comment = ctx.commentSpec() == null ? ""
+                : 
SqlLiteralUtils.parseStringLiteral(ctx.commentSpec().STRING_LITERAL().getText());
         Map<String, LoadProperty> loadPropertyMap = new HashMap<>();
         for (DorisParser.LoadPropertyContext oneLoadPropertyCOntext : 
ctx.loadProperty()) {
             LoadProperty loadProperty = 
visitLoadProperty(oneLoadPropertyCOntext);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobTest.java
index 87a9d33c27f..970b1b2e6a5 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobTest.java
@@ -29,6 +29,7 @@ import org.apache.doris.common.UserException;
 import org.apache.doris.common.jmockit.Deencapsulation;
 import org.apache.doris.datasource.InternalCatalog;
 import org.apache.doris.datasource.kafka.KafkaUtil;
+import org.apache.doris.datasource.property.fileformat.CsvFileFormatProperties;
 import org.apache.doris.load.routineload.kafka.KafkaProgress;
 import org.apache.doris.load.routineload.kafka.KafkaRoutineLoadJob;
 import org.apache.doris.load.routineload.kafka.KafkaTaskInfo;
@@ -369,6 +370,7 @@ public class RoutineLoadJobTest {
                 + "\"fuzzy_parse\" = \"false\",\n"
                 + "\"strict_mode\" = \"false\",\n"
                 + "\"timezone\" = \"Asia/Shanghai\",\n"
+                + "\"send_batch_parallelism\" = \"1\",\n"
                 + "\"exec_mem_limit\" = \"2147483648\"\n"
                 + ")\n"
                 + "FROM KAFKA\n"
@@ -413,6 +415,7 @@ public class RoutineLoadJobTest {
                 + "\"fuzzy_parse\" = \"false\",\n"
                 + "\"strict_mode\" = \"false\",\n"
                 + "\"timezone\" = \"Asia/Shanghai\",\n"
+                + "\"send_batch_parallelism\" = \"1\",\n"
                 + "\"compute_group\" = \"cg_b\",\n"
                 + "\"exec_mem_limit\" = \"2147483648\"\n"
                 + ")\n"
@@ -424,6 +427,56 @@ public class RoutineLoadJobTest {
         Assertions.assertEquals(expect, routineLoadJob.getShowCreateInfo());
     }
 
+    // The properties that change how rows are parsed and written, printed as 
literals the statement parses back
+    // from. RoutineLoadShowCreateRoundTripTest recreates jobs from the 
statement.
+    @Test
+    public void testGetShowCreateInfoCarriesWriteProperties() throws 
UserException {
+        KafkaRoutineLoadJob routineLoadJob = new KafkaRoutineLoadJob(111L, 
"test_load", 1,
+                11, "localhost:9092", "test_topic", UserIdentity.ADMIN);
+        Deencapsulation.setField(routineLoadJob, "maxErrorNum", 10);
+        Deencapsulation.setField(routineLoadJob, "maxBatchRows", 10);
+        Deencapsulation.setField(routineLoadJob, "sendBatchParallelism", 3);
+        Deencapsulation.setField(routineLoadJob, "loadToSingleTablet", true);
+        Deencapsulation.setField(routineLoadJob, "enclose", (byte) '"');
+        Deencapsulation.setField(routineLoadJob, "escape", (byte) '\\');
+        Map<String, String> jobProperties = 
Deencapsulation.getField(routineLoadJob, "jobProperties");
+        jobProperties.put(CreateRoutineLoadInfo.WORKLOAD_GROUP, "wg1");
+        jobProperties.put(CsvFileFormatProperties.PROP_EMPTY_FIELD_AS_NULL, 
"true");
+        routineLoadJob.setComment("the \"test\" job");
+
+        String expect = "CREATE ROUTINE LOAD test_load ON 11\n"
+                + "WITH APPEND\n"
+                + "PROPERTIES\n"
+                + "(\n"
+                + "\"desired_concurrent_number\" = \"0\",\n"
+                + "\"max_error_number\" = \"10\",\n"
+                + "\"max_filter_ratio\" = \"1.0\",\n"
+                + "\"max_batch_interval\" = \"60\",\n"
+                + "\"max_batch_rows\" = \"10\",\n"
+                + "\"max_batch_size\" = \"1073741824\",\n"
+                + "\"format\" = \"csv\",\n"
+                + "\"strip_outer_array\" = \"false\",\n"
+                + "\"num_as_string\" = \"false\",\n"
+                + "\"fuzzy_parse\" = \"false\",\n"
+                + "\"enclose\" = \"\"\"\",\n"
+                + "\"escape\" = \"\\\\\",\n"
+                + "\"empty_field_as_null\" = \"true\",\n"
+                + "\"strict_mode\" = \"false\",\n"
+                + "\"timezone\" = \"Asia/Shanghai\",\n"
+                + "\"send_batch_parallelism\" = \"3\",\n"
+                + "\"load_to_single_tablet\" = \"true\",\n"
+                + "\"workload_group\" = \"wg1\",\n"
+                + "\"exec_mem_limit\" = \"2147483648\"\n"
+                + ")\n"
+                + "FROM KAFKA\n"
+                + "(\n"
+                + "\"kafka_broker_list\" = \"localhost:9092\",\n"
+                + "\"kafka_topic\" = \"test_topic\"\n"
+                + ")\n"
+                + "COMMENT \"the \"\"test\"\" job\";";
+        Assertions.assertEquals(expect, routineLoadJob.getShowCreateInfo());
+    }
+
     @Test
     public void testParseUniqueKeyUpdateMode() {
         // Test valid mode strings
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadShowCreateRoundTripTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadShowCreateRoundTripTest.java
new file mode 100644
index 00000000000..da89161db68
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadShowCreateRoundTripTest.java
@@ -0,0 +1,268 @@
+// 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.load.routineload;
+
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.load.loadv2.LoadTask;
+import org.apache.doris.load.routineload.kafka.KafkaRoutineLoadJob;
+import org.apache.doris.nereids.parser.NereidsParser;
+import 
org.apache.doris.nereids.trees.plans.commands.info.CreateRoutineLoadInfo;
+import 
org.apache.doris.nereids.trees.plans.commands.load.CreateRoutineLoadCommand;
+import org.apache.doris.qe.OriginStatement;
+import org.apache.doris.qe.SqlModeHelper;
+import org.apache.doris.thrift.TPartialUpdateNewRowPolicy;
+import org.apache.doris.thrift.TUniqueKeyUpdateMode;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * SHOW CREATE ROUTINE LOAD prints the statement a job is recreated from, to 
rename it, to move it, or to start it
+ * over after STOP. Each case creates a job the way CREATE ROUTINE LOAD does, 
recreates it from what SHOW CREATE
+ * prints for it, and checks that the recreated job is defined the same way.
+ */
+public class RoutineLoadShowCreateRoundTripTest extends TestWithFeService {
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabaseAndUse("rl_db");
+        // load_to_single_tablet needs a randomly distributed table
+        createTable("CREATE TABLE rl_db.dup_tbl (k1 INT, v1 VARCHAR(32)) 
DUPLICATE KEY(k1) "
+                + "DISTRIBUTED BY RANDOM BUCKETS 1 PROPERTIES 
('replication_num' = '1')");
+        // flexible partial updates need the skip bitmap column
+        createTable("CREATE TABLE rl_db.mow_tbl (k1 INT, v1 VARCHAR(32)) 
UNIQUE KEY(k1) "
+                + "DISTRIBUTED BY HASH(k1) BUCKETS 1 PROPERTIES 
('replication_num' = '1', "
+                + "'enable_unique_key_merge_on_write' = 'true', "
+                + "'enable_unique_key_skip_bitmap_column' = 'true')");
+        createTable("CREATE TABLE rl_db.range_tbl (k1 INT, v1 VARCHAR(32)) 
DUPLICATE KEY(k1) "
+                + "PARTITION BY RANGE(k1) (PARTITION p1 VALUES LESS THAN 
('100')) "
+                + "DISTRIBUTED BY HASH(k1) BUCKETS 1 PROPERTIES 
('replication_num' = '1')");
+        alterTableSync("ALTER TABLE rl_db.range_tbl ADD TEMPORARY PARTITION 
tp1 VALUES LESS THAN ('100')");
+    }
+
+    // What RoutineLoadManager#createRoutineLoadJob does, except registering 
the job, so that the statement
+    // SHOW CREATE prints can create it again under the same name.
+    private KafkaRoutineLoadJob createJob(String sql) throws Exception {
+        CreateRoutineLoadCommand command = (CreateRoutineLoadCommand) new 
NereidsParser().parseSingle(sql);
+        CreateRoutineLoadInfo info = command.getCreateRoutineLoadInfo();
+        info.validate(connectContext);
+        KafkaRoutineLoadJob job = KafkaRoutineLoadJob.fromCreateInfo(info, 
connectContext);
+        job.setOrigStmt(new OriginStatement(sql, 0));
+        job.setComment(info.getComment());
+        return job;
+    }
+
+    // Everything CREATE ROUTINE LOAD sets on a job, as the job uses it.
+    private static Map<String, Object> definition(KafkaRoutineLoadJob job) {
+        Map<String, Object> d = new LinkedHashMap<>();
+        d.put("desired_concurrent_number", Deencapsulation.getField(job, 
"desireTaskConcurrentNum"));
+        d.put("max_error_number", Deencapsulation.getField(job, 
"maxErrorNum"));
+        d.put("max_filter_ratio", job.getMaxFilterRatio());
+        d.put("max_batch_interval", job.getMaxBatchIntervalS());
+        d.put("max_batch_rows", job.getMaxBatchRows());
+        d.put("max_batch_size", job.getMaxBatchSizeBytes());
+        d.put("strict_mode", job.isStrictMode());
+        d.put("timezone", job.getTimezone());
+        d.put("exec_mem_limit", job.getMemLimit());
+        d.put("send_batch_parallelism", job.getSendBatchParallelism());
+        d.put("load_to_single_tablet", job.isLoadToSingleTablet());
+        d.put("partial_columns", job.isFixedPartialUpdate());
+        d.put("unique_key_update_mode", job.getUniqueKeyUpdateMode());
+        d.put("partial_update_new_key_behavior", Deencapsulation.getField(job, 
"partialUpdateNewKeyPolicy"));
+        d.put("workload_group", job.getWorkloadGroup());
+        d.put("compute_group", job.getDeclaredComputeGroup());
+        d.put("format", job.getFormat());
+        d.put("jsonpaths", job.getJsonPaths());
+        d.put("strip_outer_array", job.isStripOuterArray());
+        d.put("num_as_string", job.isNumAsString());
+        d.put("fuzzy_parse", job.isFuzzyParse());
+        d.put("json_root", job.getJsonRoot());
+        d.put("enclose", job.getEnclose());
+        d.put("escape", job.getEscape());
+        d.put("empty_field_as_null", job.getEmptyFieldAsNull());
+        d.put("merge_type", job.getMergeType());
+        d.put("column_separator", job.getColumnSeparator() == null ? null : 
job.getColumnSeparator().getSeparator());
+        d.put("columns", job.getColumnExprDescs().descs.toString());
+        d.put("where", String.valueOf(job.getWhereExpr()));
+        d.put("preceding_filter", String.valueOf(job.getPrecedingFilter()));
+        d.put("delete_on", String.valueOf(job.getDeleteCondition()));
+        d.put("partitions", job.getPartitionNamesInfo() == null ? null
+                : job.getPartitionNamesInfo().isTemp() + " " + 
job.getPartitionNamesInfo().getPartitionNames());
+        d.put("comment", Deencapsulation.getField(job, "comment"));
+        d.put("data_source", job.getDataSourceProperties());
+        d.put("custom", job.getCustomProperties());
+        return d;
+    }
+
+    // Recreates the job from what SHOW CREATE prints for it, which prints the 
same statement for the new job.
+    private KafkaRoutineLoadJob assertRoundTrip(String sql) throws Exception {
+        KafkaRoutineLoadJob job = createJob(sql);
+        String showCreate = job.getShowCreateInfo();
+        KafkaRoutineLoadJob recreated = createJob(showCreate);
+        Assertions.assertEquals(definition(job), definition(recreated), 
showCreate);
+        Assertions.assertEquals(showCreate, recreated.getShowCreateInfo());
+        return job;
+    }
+
+    @Test
+    public void testCsvJob() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_csv ON dup_tbl\n"
+                + "COLUMNS TERMINATED BY ',',\n"
+                + "COLUMNS(k1, v1),\n"
+                + "WHERE k1 > 0\n"
+                + "PROPERTIES (\n"
+                + "'desired_concurrent_number' = '2',\n"
+                + "'max_error_number' = '5',\n"
+                + "'max_filter_ratio' = '0.5',\n"
+                + "'max_batch_interval' = '20',\n"
+                + "'max_batch_rows' = '300000',\n"
+                + "'max_batch_size' = '209715200',\n"
+                + "'strict_mode' = 'true',\n"
+                + "'timezone' = 'Asia/Tokyo',\n"
+                + "'exec_mem_limit' = '4294967296',\n"
+                + "'send_batch_parallelism' = '3',\n"
+                + "'load_to_single_tablet' = 'true',\n"
+                + "'workload_group' = 'normal',\n"
+                + "'format' = 'csv',\n"
+                + "'enclose' = '\"',\n"
+                + "'escape' = '\\\\',\n"
+                + "'empty_field_as_null' = 'true'\n"
+                + ")\n"
+                + "FROM KAFKA (\n"
+                + "'kafka_broker_list' = '127.0.0.1:9092',\n"
+                + "'kafka_topic' = 'topic_csv',\n"
+                + "'property.kafka_default_offsets' = 'OFFSET_BEGINNING'\n"
+                + ")\n"
+                + "COMMENT 'it''s the \"csv\" job'");
+        Assertions.assertEquals(3, job.getSendBatchParallelism());
+        Assertions.assertTrue(job.isLoadToSingleTablet());
+        Assertions.assertEquals("normal", job.getWorkloadGroup());
+        Assertions.assertEquals('"', job.getEnclose());
+        Assertions.assertEquals('\\', job.getEscape());
+        Assertions.assertTrue(job.getEmptyFieldAsNull());
+        Assertions.assertEquals("it's the \"csv\" job", 
Deencapsulation.getField(job, "comment"));
+    }
+
+    // UPDATE_FLEXIBLE_COLUMNS leaves partial_columns false, the only thing 
the statement used to carry.
+    @Test
+    public void testFlexiblePartialUpdateJob() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_flexible ON mow_tbl\n"
+                + "PROPERTIES (\n"
+                + "'format' = 'json',\n"
+                + "'unique_key_update_mode' = 'UPDATE_FLEXIBLE_COLUMNS',\n"
+                + "'partial_update_new_key_behavior' = 'ERROR',\n"
+                + "'strip_outer_array' = 'true',\n"
+                + "'num_as_string' = 'true',\n"
+                + "'json_root' = '$.data'\n"
+                + ")\n"
+                + "FROM KAFKA (\n"
+                + "'kafka_broker_list' = '127.0.0.1:9092',\n"
+                + "'kafka_topic' = 'topic_flexible'\n"
+                + ")");
+        Assertions.assertEquals(TUniqueKeyUpdateMode.UPDATE_FLEXIBLE_COLUMNS, 
job.getUniqueKeyUpdateMode());
+        Assertions.assertEquals(TPartialUpdateNewRowPolicy.ERROR,
+                Deencapsulation.getField(job, "partialUpdateNewKeyPolicy"));
+    }
+
+    // The quotes in the json paths used to end the string literal, so the 
statement did not parse.
+    @Test
+    public void testFixedPartialUpdateJobWithJsonPaths() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_fixed ON mow_tbl\n"
+                + "COLUMNS(k1, v1)\n"
+                + "PROPERTIES (\n"
+                + "'format' = 'json',\n"
+                + "'partial_columns' = 'true',\n"
+                + "'partial_update_new_key_behavior' = 'ERROR',\n"
+                + "'jsonpaths' = '[\"$.k1\",\"$.v1\"]',\n"
+                + "'strip_outer_array' = 'true',\n"
+                + "'fuzzy_parse' = 'true'\n"
+                + ")\n"
+                + "FROM KAFKA (\n"
+                + "'kafka_broker_list' = '127.0.0.1:9092',\n"
+                + "'kafka_topic' = 'topic_fixed'\n"
+                + ")");
+        Assertions.assertEquals(TUniqueKeyUpdateMode.UPDATE_FIXED_COLUMNS, 
job.getUniqueKeyUpdateMode());
+        Assertions.assertEquals("[\"$.k1\",\"$.v1\"]", job.getJsonPaths());
+    }
+
+    // The default new key policy and an UPSERT job print nothing new.
+    @Test
+    public void testFixedPartialUpdateJobWithDefaultPolicy() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_fixed_append ON mow_tbl\n"
+                + "COLUMNS(k1, v1)\n"
+                + "PROPERTIES ('format' = 'json', 'unique_key_update_mode' = 
'UPDATE_FIXED_COLUMNS')\n"
+                + "FROM KAFKA ('kafka_broker_list' = '127.0.0.1:9092', 
'kafka_topic' = 'topic_fixed_append')");
+        
Assertions.assertFalse(job.getShowCreateInfo().contains(CreateRoutineLoadInfo.PARTIAL_UPDATE_NEW_KEY_POLICY));
+        KafkaRoutineLoadJob upsert = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_upsert ON mow_tbl\n"
+                + "PROPERTIES ('format' = 'json')\n"
+                + "FROM KAFKA ('kafka_broker_list' = '127.0.0.1:9092', 
'kafka_topic' = 'topic_upsert')");
+        
Assertions.assertFalse(upsert.getShowCreateInfo().contains(CreateRoutineLoadInfo.UNIQUE_KEY_UPDATE_MODE));
+    }
+
+    @Test
+    public void testMergeJob() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_merge ON mow_tbl\n"
+                + "WITH MERGE\n"
+                + "COLUMNS TERMINATED BY '\\t',\n"
+                + "PRECEDING FILTER k1 > 1,\n"
+                + "DELETE ON v1 = 'x'\n"
+                + "FROM KAFKA ('kafka_broker_list' = '127.0.0.1:9092', 
'kafka_topic' = 'topic_merge')");
+        Assertions.assertEquals(LoadTask.MergeType.MERGE, job.getMergeType());
+    }
+
+    // TEMPORARY used to be dropped, so the statement named a formal partition 
that does not exist.
+    @Test
+    public void testTemporaryPartitions() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_temp_partition ON range_tbl\n"
+                + "TEMPORARY PARTITION(tp1)\n"
+                + "FROM KAFKA ('kafka_broker_list' = '127.0.0.1:9092', 
'kafka_topic' = 'topic_temp_partition')");
+        Assertions.assertTrue(job.getPartitionNamesInfo().isTemp());
+    }
+
+    @Test
+    public void testQuotedKafkaProperty() throws Exception {
+        KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_client_id ON dup_tbl\n"
+                + "FROM KAFKA (\n"
+                + "'kafka_broker_list' = '127.0.0.1:9092',\n"
+                + "'kafka_topic' = 'topic_client_id',\n"
+                + "'property.client.id' = 'a\"b\\\\c'\n"
+                + ")");
+        Assertions.assertEquals("a\"b\\c", 
job.getCustomProperties().get("property.client.id"));
+    }
+
+    // Under NO_BACKSLASH_ESCAPES a backslash is an ordinary character, both 
in what the session types and in
+    // what SHOW CREATE prints for it.
+    @Test
+    public void testNoBackslashEscapes() throws Exception {
+        long sqlMode = connectContext.getSessionVariable().getSqlMode();
+        
connectContext.getSessionVariable().setSqlMode(SqlModeHelper.MODE_NO_BACKSLASH_ESCAPES);
+        try {
+            KafkaRoutineLoadJob job = assertRoundTrip("CREATE ROUTINE LOAD 
rl_db.job_no_backslash_escapes ON dup_tbl\n"
+                    + "PROPERTIES ('enclose' = '\"', 'escape' = '\\')\n"
+                    + "FROM KAFKA ('kafka_broker_list' = '127.0.0.1:9092', 
'kafka_topic' = 'topic_no_escapes')");
+            Assertions.assertEquals('"', job.getEnclose());
+            Assertions.assertEquals('\\', job.getEscape());
+        } finally {
+            connectContext.getSessionVariable().setSqlMode(sqlMode);
+        }
+    }
+}


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

Reply via email to