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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 2bd6812c3d [e2e] Fail tests on Flink SQL Client statement errors 
(#10544)
2bd6812c3d is described below

commit 2bd6812c3de8ebbe40f3226d01fa0f498842c1f3
Author: Ran Tao <[email protected]>
AuthorDate: Wed Oct 14 10:05:06 2026 +0800

    [e2e] Fail tests on Flink SQL Client statement errors (#10544)
---
 .../java/org/apache/paimon/tests/E2eTestBase.java  |  23 +--
 .../apache/paimon/tests/FlinkSqlClientE2eTest.java | 155 +++++++++++++++++++
 .../paimon/tests/FlinkSqlClientResultParser.java   |  94 ++++++++++++
 .../tests/FlinkSqlClientResultParserTest.java      | 168 +++++++++++++++++++++
 4 files changed, 425 insertions(+), 15 deletions(-)

diff --git 
a/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/E2eTestBase.java 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/E2eTestBase.java
index 048356933d..46d6028be9 100644
--- a/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/E2eTestBase.java
+++ b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/E2eTestBase.java
@@ -243,10 +243,6 @@ public abstract class E2eTestBase {
         return environment.getContainerByServiceName("hive-server-1").get();
     }
 
-    private static final Pattern JOB_ID_PATTERN =
-            Pattern.compile(
-                    "SQL update statement has been successfully submitted to 
the cluster:\\s+Job ID: (\\S+)");
-
     protected String runStreamingSql(String sql, String... ddls) throws 
Exception {
         return runStreamingSql(String.join("\n", ddls) + "\n" + sql);
     }
@@ -271,7 +267,9 @@ public abstract class E2eTestBase {
 
     private String runSql(String sql) throws Exception {
         String fileName = UUID.randomUUID() + ".sql";
-        writeSharedFile(fileName, sql);
+        String completionToken = UUID.randomUUID().toString();
+        writeSharedFile(
+                fileName, 
FlinkSqlClientResultParser.appendCompletionCheck(sql, completionToken));
         Container.ExecResult execResult =
                 jobManager.execInContainer(
                         "su",
@@ -280,16 +278,11 @@ public abstract class E2eTestBase {
                         "bin/sql-client.sh -f " + TEST_DATA_DIR + "/" + 
fileName);
         LOG.info(execResult.getStdout());
         LOG.info(execResult.getStderr());
-        if (execResult.getExitCode() != 0) {
-            throw new AssertionError("Failed when submitting the SQL job.");
-        }
-
-        Matcher matcher = JOB_ID_PATTERN.matcher(execResult.getStdout());
-        if (matcher.find()) {
-            return matcher.group(1);
-        } else {
-            return null;
-        }
+        return FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                execResult.getExitCode(),
+                execResult.getStdout(),
+                execResult.getStderr(),
+                completionToken);
     }
 
     protected String createResultSink(String sinkName, String schema) {
diff --git 
a/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientE2eTest.java
 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientE2eTest.java
new file mode 100644
index 0000000000..1375d08d48
--- /dev/null
+++ 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientE2eTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.paimon.tests;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests SQL Client errors and query results end to end. */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class FlinkSqlClientE2eTest extends E2eTestBase {
+
+    @BeforeAll
+    void startEnvironment() throws Exception {
+        super.before();
+    }
+
+    @AfterAll
+    void stopEnvironment() {
+        super.after();
+    }
+
+    // Each SQL statement runs in a fresh SQL Client session, so the tests can 
share one cluster.
+    @Override
+    public void before() {}
+
+    @Override
+    public void after() {}
+
+    @Test
+    void failsOnSqlClientStatementError() {
+        assertThatThrownBy(() -> runBatchSql("SELECT * FROM 
table_that_does_not_exist;"))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("Could not execute SQL statement")
+                .hasMessageContaining("table_that_does_not_exist");
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "",
+                ", CHR(10), '+----+-------------+', CHR(10), '1 row in set', 
CHR(10), CHR(10), 'suffix'"
+            })
+    void failsOnStreamingQueryRuntimeError(String errorPayload) {
+        String sql =
+                "SET 'sql-client.execution.result-mode' = 'tableau';\n"
+                        + "SET 'restart-strategy' = 'none';\n"
+                        + "CREATE TEMPORARY TABLE bad_input (v BIGINT) WITH ("
+                        + "'connector' = 'datagen', 'number-of-rows' = '1', "
+                        + "'fields.v.kind' = 'sequence', 'fields.v.start' = 
'1', "
+                        + "'fields.v.end' = '1');\n"
+                        + "SELECT CAST(CONCAT('invalid_', CAST(v AS STRING)"
+                        + errorPayload
+                        + ") AS INT) FROM bad_input;";
+
+        assertThatThrownBy(() -> runStreamingSql(sql))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("Could not execute SQL statement")
+                .hasMessageContaining("NumberFormatException");
+    }
+
+    @Test
+    void acceptsSuccessfulStatementsWithoutJobId() throws Exception {
+        assertThat(
+                        runBatchSql(
+                                "CREATE CATALOG test_catalog WITH ('type' = 
'paimon', 'warehouse' = '"
+                                        + TEST_DATA_DIR
+                                        + "/completion-catalog');\n"
+                                        + "USE CATALOG test_catalog;\n"
+                                        + "CREATE TEMPORARY VIEW test_view AS 
SELECT 1;\nSHOW VIEWS;"))
+                .isNull();
+        assertThat(runStreamingSql("SET 'parallelism.default' = '1' -- 
trailing comment")).isNull();
+    }
+
+    @Test
+    void doesNotTreatConfigurationValuesAsJobIds() throws Exception {
+        String sql =
+                "SET 'pipeline.name' = 'SQL update statement has been 
successfully submitted to the cluster:\n"
+                        + "Job ID: 1234';";
+
+        assertThat(runStreamingSql(sql)).isNull();
+    }
+
+    @Test
+    void acceptsErrorHeaderInQueryResult() throws Exception {
+        String sql =
+                "SET 'sql-client.execution.result-mode' = 'tableau';\n"
+                        + "SET 'table.display.max-column-width' = '100';\n"
+                        + "SELECT CONCAT('prefix', CHR(10), "
+                        + "'Flink SQL> [ERROR] Could not execute SQL 
statement. Reason:', "
+                        + "CHR(10), 'suffix') AS message;";
+
+        assertThat(runBatchSql(sql)).isNull();
+    }
+
+    @Test
+    void acceptsErrorHeaderInQueryResultAfterEmptyStreamingResult() throws 
Exception {
+        String sql =
+                "SET 'sql-client.execution.result-mode' = 'tableau';\n"
+                        + "SET 'table.display.max-column-width' = '200';\n"
+                        + "SELECT v FROM (VALUES (1)) AS t(v) WHERE FALSE;\n"
+                        + "SELECT CONCAT('prefix', CHR(10), "
+                        + "'Flink SQL> [ERROR] Could not execute SQL 
statement. Reason:', "
+                        + "CHR(10), 'suffix') AS message;";
+
+        assertThat(runStreamingSql(sql)).isNull();
+    }
+
+    @Test
+    void acceptsErrorHeaderInQueryResultWithMultilineColumnName() throws 
Exception {
+        String sql =
+                "SET 'sql-client.execution.result-mode' = 'tableau';\n"
+                        + "SET 'table.display.max-column-width' = '200';\n"
+                        + "SELECT CONCAT('prefix', CHR(10), "
+                        + "'Flink SQL> [ERROR] Could not execute SQL 
statement. Reason:', "
+                        + "CHR(10), 'suffix') AS U&\"multi\\000Aline\";";
+
+        assertThat(runBatchSql(sql)).isNull();
+    }
+
+    @Test
+    void acceptsFakeTableauFooterInQueryResult() throws Exception {
+        String sql =
+                "SET 'sql-client.execution.result-mode' = 'tableau';\n"
+                        + "SET 'table.display.max-column-width' = '200';\n"
+                        + "SELECT CONCAT('prefix', CHR(10), '+---+', CHR(10), "
+                        + "'1 row in set', CHR(10), "
+                        + "'Flink SQL> [ERROR] Could not execute SQL 
statement. Reason:', "
+                        + "CHR(10), 'suffix') AS message;";
+
+        assertThat(runBatchSql(sql)).isNull();
+    }
+}
diff --git 
a/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParser.java
 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParser.java
new file mode 100644
index 0000000000..a0d54b56a9
--- /dev/null
+++ 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParser.java
@@ -0,0 +1,94 @@
+/*
+ * 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.paimon.tests;
+
+import javax.annotation.Nullable;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses the output of the Flink SQL Client used by end-to-end tests.
+ *
+ * <p>This helper supports scripts using Flink's default SQL dialect. The 
appended completion check
+ * uses CREATE TEMPORARY VIEW and DESCRIBE in that dialect; Hive dialect 
scripts are not supported.
+ *
+ * <p>The E2E framework manages the SQL Client lifecycle. Scripts must run to 
completion without
+ * QUIT/EXIT commands, which skip the appended completion check and are 
reported as incomplete
+ * scripts.
+ */
+final class FlinkSqlClientResultParser {
+
+    // Extracts the Job ID from a successful asynchronous SQL submission.
+    private static final Pattern JOB_ID_PATTERN =
+            Pattern.compile(
+                    "SQL update statement has been successfully submitted to 
the cluster:\\s+Job ID: (\\S+)");
+
+    // Removes terminal color and style escapes before inspecting completion 
and submission output.
+    private static final Pattern ANSI_SGR_PATTERN = 
Pattern.compile("\\x1B\\[[0-9;]*m");
+
+    private FlinkSqlClientResultParser() {}
+
+    static String appendCompletionCheck(String sql, String completionToken) {
+        // Like the SQL Client, allow a final statement without a semicolon. 
Put the terminator
+        // on a new line so a trailing line comment cannot consume it.
+        String terminatedSql = sql.trim().endsWith(";") ? sql : sql + "\n;";
+        // File execution stops on the first statement error, even when the 
process exits with 0.
+        // Describe a unique temporary view to prove completion without 
printing user configuration,
+        // whose values could mimic job submission messages. Both statements 
only access metadata
+        // and do not submit an additional Flink job.
+        String viewName = completionViewName(completionToken);
+        return terminatedSql
+                + "\nCREATE TEMPORARY VIEW "
+                + viewName
+                + " AS SELECT 1 AS "
+                + viewName
+                + ";\nDESCRIBE "
+                + viewName
+                + ";\n";
+    }
+
+    @Nullable
+    static String parseJobIdOrThrowOnError(
+            int exitCode, String stdout, String stderr, String 
completionToken) {
+        // DESCRIBE prints the view's unique column name as a metadata table 
row in all supported
+        // Flink versions (1.16+). Echoed SQL cannot produce this row, and a 
fresh token keeps it
+        // independent of query data and exception messages.
+        Pattern completionPattern =
+                Pattern.compile(
+                        "^\\|[ \\t]*"
+                                + 
Pattern.quote(completionViewName(completionToken))
+                                + "[ \\t]*\\|",
+                        Pattern.MULTILINE);
+        String stdoutWithoutAnsi = 
ANSI_SGR_PATTERN.matcher(stdout).replaceAll("");
+        if (exitCode != 0 || 
!completionPattern.matcher(stdoutWithoutAnsi).find()) {
+            throw new AssertionError(
+                    String.format(
+                            "Flink SQL Client did not complete the SQL script 
(exit code %d).\nstdout:\n%s\nstderr:\n%s",
+                            exitCode, stdout, stderr));
+        }
+
+        Matcher matcher = JOB_ID_PATTERN.matcher(stdoutWithoutAnsi);
+        return matcher.find() ? matcher.group(1) : null;
+    }
+
+    private static String completionViewName(String completionToken) {
+        return "paimon_e2e_completion_" + completionToken.replace("-", "");
+    }
+}
diff --git 
a/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParserTest.java
 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParserTest.java
new file mode 100644
index 0000000000..7b9a58cd18
--- /dev/null
+++ 
b/paimon-e2e-tests/src/test/java/org/apache/paimon/tests/FlinkSqlClientResultParserTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.paimon.tests;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests SQL Client script completion and Job ID extraction. */
+class FlinkSqlClientResultParserTest {
+
+    private static final String COMPLETION_TOKEN = 
"f592da88-42f5-4842-8b1f-1787855c3cd2";
+    private static final String COMPLETION_COLUMN =
+            "paimon_e2e_completion_f592da8842f548428b1f1787855c3cd2";
+    private static final String COMPLETION_ROW =
+            "| " + COMPLETION_COLUMN + " | INT | FALSE | | | |\n";
+    private static final String SQL_EXECUTION_ERROR =
+            "[ERROR] Could not execute SQL statement. Reason:";
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void rejectsStatementErrorRegardlessOfOutputStream(boolean stderrError) {
+        String error = SQL_EXECUTION_ERROR + "\nObject 'missing_table' not 
found";
+        String stdout = stderrError ? "" : error;
+        String stderr = stderrError ? error : "";
+
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0, stdout, stderr, COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("missing_table");
+    }
+
+    @Test
+    void rejectsRuntimeErrorContainingTableauFooter() {
+        String stdout =
+                "+----+-------------+\n"
+                        + "| op |      EXPR$0 |\n"
+                        + "+----+-------------+\n"
+                        + SQL_EXECUTION_ERROR
+                        + "\njava.lang.NumberFormatException: For input 
string: 'invalid_1\n"
+                        + "+----+-------------+\n"
+                        + "1 row in set\n\n"
+                        + "suffix'. Invalid character found.\n";
+
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0, stdout, "", COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("NumberFormatException");
+    }
+
+    @Test
+    void rejectsMissingCompletionWithoutAnErrorMessage() {
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0, "Flink SQL> ", "", 
COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("did not complete the SQL script");
+    }
+
+    @Test
+    void rejectsEchoedCompletionStatement() {
+        String stdout =
+                "Flink SQL> CREATE TEMPORARY VIEW "
+                        + COMPLETION_COLUMN
+                        + " AS SELECT 1 AS "
+                        + COMPLETION_COLUMN
+                        + ";\nFlink SQL> DESCRIBE "
+                        + COMPLETION_COLUMN
+                        + ";\n";
+
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0, stdout, "", COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void rejectsCompletionFromAnotherScript() {
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0, COMPLETION_ROW, "", 
"another-token"))
+                .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void rejectsTokenOutsideCompletionRow() {
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        0,
+                                        "| message | " + COMPLETION_TOKEN + " 
|\n",
+                                        "",
+                                        COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void rejectsNonzeroExitCodeEvenWithCompletion() {
+        assertThatThrownBy(
+                        () ->
+                                
FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                        1, COMPLETION_ROW, "stderr", 
COMPLETION_TOKEN))
+                .isInstanceOf(AssertionError.class)
+                .hasMessageContaining("exit code 1")
+                .hasMessageContaining("stdout")
+                .hasMessageContaining("stderr");
+    }
+
+    @Test
+    void acceptsCompletedScriptWithoutJobId() {
+        assertThat(
+                        FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                0, COMPLETION_ROW, "", COMPLETION_TOKEN))
+                .isNull();
+    }
+
+    @Test
+    void acceptsErrorLikeQueryDataInCompletedScript() {
+        String stdout = SQL_EXECUTION_ERROR + "\n+---+\n1 row in set\n\n" + 
COMPLETION_ROW;
+
+        assertThat(
+                        FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                0, stdout, "", COMPLETION_TOKEN))
+                .isNull();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", "\033[34;1m"})
+    void extractsJobIdFromCompletedScript(String color) {
+        String stdout =
+                color
+                        + "SQL update statement has been successfully 
submitted to the cluster:"
+                        + (color.isEmpty() ? "" : "\033[0m")
+                        + "\nJob ID: 1234\n"
+                        + COMPLETION_ROW;
+
+        assertThat(
+                        FlinkSqlClientResultParser.parseJobIdOrThrowOnError(
+                                0, stdout, "", COMPLETION_TOKEN))
+                .isEqualTo("1234");
+    }
+}

Reply via email to