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

nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-paimon-webui.git


The following commit(s) were added to refs/heads/main by this push:
     new 663965a  [Feature] Introduce Flink SQL gateway baseline implementation 
of job submission (#153)
663965a is described below

commit 663965abca32d59d75d912ebbe5f8f3b2aa8ce75
Author: s7monk <[email protected]>
AuthorDate: Thu Feb 1 16:54:30 2024 +0800

    [Feature] Introduce Flink SQL gateway baseline implementation of job 
submission (#153)
---
 .../web/engine/flink/common/executor/Executor.java |   3 +-
 .../flink/common/executor/ExecutorFactory.java     |  31 +++
 .../paimon-web-engine-flink-sql-gateway}/pom.xml   |  18 +-
 .../flink/sql/gateway/client/SqlGatewayClient.java | 245 +++++++++++++++++++++
 .../gateway/executor/FlinkSqlGatewayExecutor.java  | 163 ++++++++++++++
 .../executor/FlinkSqlGatewayExecutorFactory.java   |  38 ++++
 .../flink/sql/gateway/model/SessionEntity.java     |  37 ++++
 .../flink/sql/gateway/utils/CollectResultUtil.java |  63 ++++++
 .../gateway/utils/FlinkSqlStatementSetBuilder.java |  61 +++++
 .../sql/gateway/utils/SqlGateWayRestClient.java    | 147 +++++++++++++
 .../web/engine/flink/sql/gataway/TestBase.java     | 201 +++++++++++++++++
 .../sql/gataway/client/SqlGatewayClientTest.java   | 109 +++++++++
 .../executor/FlinkSqlGatewayExecutorTest.java      | 116 ++++++++++
 .../sql/gataway/executor/StatementsConstant.java   | 196 +++++++++++++++++
 paimon-web-engine/paimon-web-engine-flink/pom.xml  |   1 +
 paimon-web-engine/pom.xml                          |   8 +
 16 files changed, 1427 insertions(+), 10 deletions(-)

diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/Executor.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/Executor.java
index 64218a1..60a754f 100644
--- 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/Executor.java
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/Executor.java
@@ -50,8 +50,7 @@ public interface Executor {
      *
      * @param jobId The unique identifier of the job to stop.
      * @param withSavepoint If true, the job will create a savepoint before 
stopping.
-     * @return true if the job was successfully stopped, false otherwise.
      * @throws Exception if the job cannot be stopped or savepoint cannot be 
created.
      */
-    boolean stop(String jobId, boolean withSavepoint) throws Exception;
+    void stop(String jobId, boolean withSavepoint) throws Exception;
 }
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/ExecutorFactory.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/ExecutorFactory.java
new file mode 100644
index 0000000..5d87fe0
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-common/src/main/java/org/apache/paimon/web/engine/flink/common/executor/ExecutorFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.web.engine.flink.common.executor;
+
+/** Factory to create {@link Executor}. */
+public interface ExecutorFactory {
+
+    /**
+     * Creates a new {@link Executor}.
+     *
+     * @return A new instance of {@link Executor}
+     * @throws Exception if executor creation fails
+     */
+    Executor createExecutor() throws Exception;
+}
diff --git a/paimon-web-engine/pom.xml 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/pom.xml
similarity index 72%
copy from paimon-web-engine/pom.xml
copy to 
paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/pom.xml
index 81a45f5..3fc87cf 100644
--- a/paimon-web-engine/pom.xml
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/pom.xml
@@ -24,16 +24,18 @@ under the License.
 
     <parent>
         <groupId>org.apache.paimon</groupId>
-        <artifactId>paimon-webui</artifactId>
+        <artifactId>paimon-web-engine-flink</artifactId>
         <version>0.1-SNAPSHOT</version>
     </parent>
 
-    <artifactId>paimon-web-engine</artifactId>
-    <name>Paimon : Web : Engine</name>
+    <artifactId>paimon-web-engine-flink-sql-gateway</artifactId>
+    <name>Paimon : Web : Engine : Flink : SQL : Gateway</name>
 
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>paimon-web-engine-flink</module>
-    </modules>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.paimon</groupId>
+            <artifactId>paimon-web-engine-flink-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
 </project>
\ No newline at end of file
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/client/SqlGatewayClient.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/client/SqlGatewayClient.java
new file mode 100644
index 0000000..f84c699
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/client/SqlGatewayClient.java
@@ -0,0 +1,245 @@
+/*
+ * 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.web.engine.flink.sql.gateway.client;
+
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import 
org.apache.paimon.web.engine.flink.sql.gateway.utils.SqlGateWayRestClient;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.flink.runtime.rest.messages.EmptyMessageParameters;
+import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
+import org.apache.flink.table.gateway.api.operation.OperationHandle;
+import org.apache.flink.table.gateway.api.results.ResultSet;
+import org.apache.flink.table.gateway.api.session.SessionHandle;
+import 
org.apache.flink.table.gateway.rest.header.operation.CancelOperationHeaders;
+import 
org.apache.flink.table.gateway.rest.header.operation.CloseOperationHeaders;
+import 
org.apache.flink.table.gateway.rest.header.operation.GetOperationStatusHeaders;
+import org.apache.flink.table.gateway.rest.header.session.CloseSessionHeaders;
+import 
org.apache.flink.table.gateway.rest.header.session.ConfigureSessionHeaders;
+import 
org.apache.flink.table.gateway.rest.header.session.GetSessionConfigHeaders;
+import org.apache.flink.table.gateway.rest.header.session.OpenSessionHeaders;
+import 
org.apache.flink.table.gateway.rest.header.session.TriggerSessionHeartbeatHeaders;
+import 
org.apache.flink.table.gateway.rest.header.statement.CompleteStatementHeaders;
+import 
org.apache.flink.table.gateway.rest.header.statement.ExecuteStatementHeaders;
+import 
org.apache.flink.table.gateway.rest.header.statement.FetchResultsHeaders;
+import 
org.apache.flink.table.gateway.rest.message.operation.OperationMessageParameters;
+import 
org.apache.flink.table.gateway.rest.message.session.ConfigureSessionRequestBody;
+import 
org.apache.flink.table.gateway.rest.message.session.GetSessionConfigResponseBody;
+import 
org.apache.flink.table.gateway.rest.message.session.OpenSessionRequestBody;
+import 
org.apache.flink.table.gateway.rest.message.session.SessionMessageParameters;
+import 
org.apache.flink.table.gateway.rest.message.statement.CompleteStatementRequestBody;
+import 
org.apache.flink.table.gateway.rest.message.statement.ExecuteStatementRequestBody;
+import 
org.apache.flink.table.gateway.rest.message.statement.FetchResultsMessageParameters;
+import 
org.apache.flink.table.gateway.rest.message.statement.FetchResultsResponseBody;
+import org.apache.flink.table.gateway.rest.util.RowFormat;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The client of flink sql gateway provides some operations of flink sql 
gateway. such as creating
+ * session, execute statement, fetch result, etc.
+ */
+public class SqlGatewayClient {
+
+    private static final String DEFAULT_SESSION_NAME_PREFIX = 
"FLINK_SQL_GATEWAY_SESSION";
+    private static final int REQUEST_WAITE_TIME = 1000;
+    private static final int ACTIVE_STATUS = 1;
+
+    private final SqlGateWayRestClient restClient;
+    private final String sqlGatewayHost;
+    private final int sqlGatewayPort;
+
+    public SqlGatewayClient(String sqlGatewayHost, int sqlGatewayPort) throws 
Exception {
+        this.sqlGatewayHost = sqlGatewayHost;
+        this.sqlGatewayPort = sqlGatewayPort;
+        this.restClient = new SqlGateWayRestClient(sqlGatewayHost, 
sqlGatewayPort);
+    }
+
+    public SessionEntity openSession(String sessionName) throws Exception {
+        String name =
+                StringUtils.isBlank(sessionName)
+                        ? DEFAULT_SESSION_NAME_PREFIX + "_" + UUID.randomUUID()
+                        : sessionName;
+
+        String sessionId =
+                restClient
+                        .sendRequest(
+                                OpenSessionHeaders.getInstance(),
+                                EmptyMessageParameters.getInstance(),
+                                new OpenSessionRequestBody(name, new 
HashMap<>()))
+                        .get()
+                        .getSessionHandle();
+
+        return SessionEntity.builder()
+                .sessionId(sessionId)
+                .sessionName(sessionName)
+                .host(sqlGatewayHost)
+                .port(sqlGatewayPort)
+                .properties(getSessionConfig(sessionId))
+                .status(ACTIVE_STATUS)
+                .build();
+    }
+
+    public Map<String, String> getSessionConfig(String sessionId) throws 
Exception {
+        GetSessionConfigResponseBody getSessionConfigResponseBody =
+                restClient
+                        .sendRequest(
+                                GetSessionConfigHeaders.getInstance(),
+                                new SessionMessageParameters(
+                                        
buildSessionHandleBySessionId(sessionId)),
+                                EmptyRequestBody.getInstance())
+                        .get();
+        return getSessionConfigResponseBody.getProperties();
+    }
+
+    public void configureSession(String sessionId, String statement, Long 
timeout)
+            throws Exception {
+        ConfigureSessionRequestBody configureSessionRequestBody =
+                new ConfigureSessionRequestBody(statement, timeout);
+        restClient
+                .sendRequest(
+                        ConfigureSessionHeaders.getInstance(),
+                        new 
SessionMessageParameters(buildSessionHandleBySessionId(sessionId)),
+                        configureSessionRequestBody)
+                .get();
+    }
+
+    public String closeSession(String sessionId) throws Exception {
+        return restClient
+                .sendRequest(
+                        CloseSessionHeaders.getInstance(),
+                        new 
SessionMessageParameters(buildSessionHandleBySessionId(sessionId)),
+                        EmptyRequestBody.getInstance())
+                .get()
+                .getStatus();
+    }
+
+    public void triggerSessionHeartbeat(String sessionId) throws Exception {
+        restClient
+                .sendRequest(
+                        TriggerSessionHeartbeatHeaders.getInstance(),
+                        new 
SessionMessageParameters(buildSessionHandleBySessionId(sessionId)),
+                        EmptyRequestBody.getInstance())
+                .get();
+    }
+
+    public String executeStatement(String sessionId, String statement, 
@Nullable Long timeout)
+            throws Exception {
+        return restClient
+                .sendRequest(
+                        ExecuteStatementHeaders.getInstance(),
+                        new 
SessionMessageParameters(buildSessionHandleBySessionId(sessionId)),
+                        new ExecuteStatementRequestBody(statement, timeout, 
new HashMap<>()))
+                .get()
+                .getOperationHandle();
+    }
+
+    public List<String> completeStatementHints(String sessionId, String 
statement)
+            throws Exception {
+        return restClient
+                .sendRequest(
+                        CompleteStatementHeaders.getInstance(),
+                        new 
SessionMessageParameters(buildSessionHandleBySessionId(sessionId)),
+                        new CompleteStatementRequestBody(statement, 
statement.length()))
+                .get()
+                .getCandidates();
+    }
+
+    public FetchResultsResponseBody fetchResults(String sessionId, String 
operationId, long token)
+            throws Exception {
+        FetchResultsResponseBody fetchResultsResponseBody =
+                restClient
+                        .sendRequest(
+                                FetchResultsHeaders.getDefaultInstance(),
+                                new FetchResultsMessageParameters(
+                                        
buildSessionHandleBySessionId(sessionId),
+                                        
buildOperationHandleByOperationId(operationId),
+                                        token,
+                                        RowFormat.JSON),
+                                EmptyRequestBody.getInstance())
+                        .get();
+        ResultSet.ResultType resultType = 
fetchResultsResponseBody.getResultType();
+        while (resultType == ResultSet.ResultType.NOT_READY) {
+            fetchResultsResponseBody =
+                    restClient
+                            .sendRequest(
+                                    FetchResultsHeaders.getDefaultInstance(),
+                                    new FetchResultsMessageParameters(
+                                            
buildSessionHandleBySessionId(sessionId),
+                                            
buildOperationHandleByOperationId(operationId),
+                                            token,
+                                            RowFormat.JSON),
+                                    EmptyRequestBody.getInstance())
+                            .get();
+            resultType = fetchResultsResponseBody.getResultType();
+            TimeUnit.MILLISECONDS.sleep(REQUEST_WAITE_TIME);
+        }
+        return fetchResultsResponseBody;
+    }
+
+    public String getOperationStatus(String sessionId, String operationId) 
throws Exception {
+        return restClient
+                .sendRequest(
+                        GetOperationStatusHeaders.getInstance(),
+                        new OperationMessageParameters(
+                                buildSessionHandleBySessionId(sessionId),
+                                
buildOperationHandleByOperationId(operationId)),
+                        EmptyRequestBody.getInstance())
+                .get()
+                .getStatus();
+    }
+
+    public String cancelOperation(String sessionId, String operationId) throws 
Exception {
+        return restClient
+                .sendRequest(
+                        CancelOperationHeaders.getInstance(),
+                        new OperationMessageParameters(
+                                buildSessionHandleBySessionId(sessionId),
+                                
buildOperationHandleByOperationId(operationId)),
+                        EmptyRequestBody.getInstance())
+                .get()
+                .getStatus();
+    }
+
+    public String closeOperation(String sessionId, String operationId) throws 
Exception {
+        return restClient
+                .sendRequest(
+                        CloseOperationHeaders.getInstance(),
+                        new OperationMessageParameters(
+                                buildSessionHandleBySessionId(sessionId),
+                                
buildOperationHandleByOperationId(operationId)),
+                        EmptyRequestBody.getInstance())
+                .get()
+                .getStatus();
+    }
+
+    private SessionHandle buildSessionHandleBySessionId(String sessionId) {
+        return new SessionHandle(UUID.fromString(sessionId));
+    }
+
+    private OperationHandle buildOperationHandleByOperationId(String 
operationId) {
+        return new OperationHandle(UUID.fromString(operationId));
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutor.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutor.java
new file mode 100644
index 0000000..12c5b4d
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutor.java
@@ -0,0 +1,163 @@
+/*
+ * 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.web.engine.flink.sql.gateway.executor;
+
+import org.apache.paimon.web.engine.flink.common.executor.Executor;
+import 
org.apache.paimon.web.engine.flink.common.operation.FlinkSqlOperationType;
+import org.apache.paimon.web.engine.flink.common.parser.StatementParser;
+import org.apache.paimon.web.engine.flink.common.result.ExecutionResult;
+import org.apache.paimon.web.engine.flink.common.result.FetchResultParams;
+import org.apache.paimon.web.engine.flink.sql.gateway.client.SqlGatewayClient;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import org.apache.paimon.web.engine.flink.sql.gateway.utils.CollectResultUtil;
+import 
org.apache.paimon.web.engine.flink.sql.gateway.utils.FlinkSqlStatementSetBuilder;
+
+import org.apache.flink.table.gateway.api.results.ResultSet;
+import 
org.apache.flink.table.gateway.rest.message.statement.FetchResultsResponseBody;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** The flink sql gateway implementation of the {@link Executor}. */
+public class FlinkSqlGatewayExecutor implements Executor {
+
+    private static final Long DEFAULT_FETCH_TOKEN = 0L;
+    private static final String STOP_JOB_BASE_SQL = "STOP JOB '%s'";
+    private static final String WITH_SAVEPOINT = " WITH SAVEPOINT";
+
+    private final SqlGatewayClient client;
+    private final SessionEntity session;
+
+    public FlinkSqlGatewayExecutor(SessionEntity session) throws Exception {
+        this.session = session;
+        this.client = new SqlGatewayClient(session.getHost(), 
session.getPort());
+    }
+
+    @Override
+    public ExecutionResult executeSql(String multiStatement) throws Exception {
+        String[] statements = StatementParser.parse(multiStatement);
+        List<String> insertStatements = new ArrayList<>();
+        ExecutionResult executionResult = null;
+
+        for (String statement : statements) {
+            FlinkSqlOperationType operationType = 
FlinkSqlOperationType.getOperationType(statement);
+
+            if (operationType == null) {
+                String operationTypeString = 
extractSqlOperationType(statement);
+                throw new UnsupportedOperationException(
+                        "Unsupported operation type: " + operationTypeString);
+            }
+
+            switch (operationType.getCategory()) {
+                case DQL:
+                    if (!insertStatements.isEmpty()) {
+                        throw new UnsupportedOperationException(
+                                "Cannot execute DQL statement with pending 
INSERT statements.");
+                    }
+                    executionResult = executeDqlStatement(statement, 
operationType);
+                    break;
+                case DML:
+                    if 
(operationType.getType().equals(FlinkSqlOperationType.INSERT.getType())) {
+                        insertStatements.add(statement);
+                    } else {
+                        executionResult = executeDmlStatement(statement);
+                    }
+                    break;
+                default:
+                    client.executeStatement(session.getSessionId(), statement, 
null);
+                    break;
+            }
+
+            if (executionResult != null) {
+                return executionResult;
+            }
+        }
+
+        if (!insertStatements.isEmpty()) {
+            String combinedStatement =
+                    
FlinkSqlStatementSetBuilder.buildStatementSet(insertStatements);
+            executionResult = executeDmlStatement(combinedStatement);
+        }
+
+        return executionResult;
+    }
+
+    private ExecutionResult executeDqlStatement(
+            String statement, FlinkSqlOperationType operationType) throws 
Exception {
+        String operationId = client.executeStatement(session.getSessionId(), 
statement, null);
+        FetchResultsResponseBody results =
+                client.fetchResults(session.getSessionId(), operationId, 
DEFAULT_FETCH_TOKEN);
+        ExecutionResult.Builder builder =
+                CollectResultUtil.collectSqlGatewayResult(results.getResults())
+                        .submitId(operationId);
+        if 
(operationType.getType().equals(FlinkSqlOperationType.SELECT.getType())) {
+            
builder.jobId(getJobIdFromResults(results)).shouldFetchResult(true);
+        }
+        return builder.build();
+    }
+
+    private ExecutionResult executeDmlStatement(String statement) throws 
Exception {
+        String operationId = client.executeStatement(session.getSessionId(), 
statement, null);
+        FetchResultsResponseBody results =
+                client.fetchResults(session.getSessionId(), operationId, 
DEFAULT_FETCH_TOKEN);
+        return new ExecutionResult.Builder()
+                .submitId(operationId)
+                .jobId(getJobIdFromResults(results))
+                .build();
+    }
+
+    private String getJobIdFromResults(FetchResultsResponseBody results) {
+        return Objects.requireNonNull(results.getJobID(), "Job ID not found in 
results").toString();
+    }
+
+    @Override
+    public ExecutionResult fetchResults(FetchResultParams params) throws 
Exception {
+        FetchResultsResponseBody fetchResultsResponseBody =
+                client.fetchResults(params.getSessionId(), 
params.getSubmitId(), params.getToken());
+        ResultSet.ResultType resultType = 
fetchResultsResponseBody.getResultType();
+        if (resultType == ResultSet.ResultType.EOS) {
+            return ExecutionResult.builder().shouldFetchResult(false).build();
+        }
+        ExecutionResult.Builder builder =
+                
CollectResultUtil.collectSqlGatewayResult(fetchResultsResponseBody.getResults());
+        builder.submitId(params.getSubmitId());
+        return builder.build();
+    }
+
+    @Override
+    public void stop(String jobId, boolean withSavepoint) throws Exception {
+        StringBuilder sqlBuilder = new 
StringBuilder(String.format(STOP_JOB_BASE_SQL, jobId));
+        if (withSavepoint) {
+            sqlBuilder.append(WITH_SAVEPOINT);
+        }
+        client.executeStatement(session.getSessionId(), sqlBuilder.toString(), 
null);
+    }
+
+    private String extractSqlOperationType(String sql) {
+        Pattern pattern = Pattern.compile("^(\\w+)");
+        Matcher matcher = pattern.matcher(sql);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        return "UNKNOWN";
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutorFactory.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutorFactory.java
new file mode 100644
index 0000000..be05dde
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/executor/FlinkSqlGatewayExecutorFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.web.engine.flink.sql.gateway.executor;
+
+import org.apache.paimon.web.engine.flink.common.executor.Executor;
+import org.apache.paimon.web.engine.flink.common.executor.ExecutorFactory;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+
+/** Factory to create {@link FlinkSqlGatewayExecutor}. */
+public class FlinkSqlGatewayExecutorFactory implements ExecutorFactory {
+
+    private final SessionEntity sessionEntity;
+
+    public FlinkSqlGatewayExecutorFactory(SessionEntity sessionEntity) {
+        this.sessionEntity = sessionEntity;
+    }
+
+    @Override
+    public Executor createExecutor() throws Exception {
+        return new FlinkSqlGatewayExecutor(sessionEntity);
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/model/SessionEntity.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/model/SessionEntity.java
new file mode 100644
index 0000000..c0bf8a0
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/model/SessionEntity.java
@@ -0,0 +1,37 @@
+/*
+ * 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.web.engine.flink.sql.gateway.model;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.Map;
+
+/** The session entity. */
+@Builder
+@Getter
+public class SessionEntity {
+
+    private final String sessionId;
+    private final String sessionName;
+    private final String host;
+    private final int port;
+    private final Map<String, String> properties;
+    private final int status;
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/CollectResultUtil.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/CollectResultUtil.java
new file mode 100644
index 0000000..8ec6f45
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/CollectResultUtil.java
@@ -0,0 +1,63 @@
+/*
+ * 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.web.engine.flink.sql.gateway.utils;
+
+import org.apache.paimon.web.engine.flink.common.result.ExecutionResult;
+
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.gateway.rest.serde.ResultInfo;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** Collect result util. */
+public class CollectResultUtil {
+
+    public static ExecutionResult.Builder collectSqlGatewayResult(ResultInfo 
resultInfo) {
+        List<RowData> data = resultInfo.getData();
+        List<Map<String, Object>> results =
+                rowDatasToList(resultInfo.getResultSchema().getColumnNames(), 
data);
+        return ExecutionResult.builder().data(results);
+    }
+
+    private static List<Map<String, Object>> rowDatasToList(
+            List<String> columns, List<RowData> rowDataList) {
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (RowData rowData : rowDataList) {
+            Map<String, Object> map = new LinkedHashMap<>();
+            for (int i = 0; i < rowData.getArity(); ++i) {
+                if (rowData instanceof GenericRowData) {
+                    GenericRowData data = (GenericRowData) rowData;
+                    Object field = data.getField(i);
+                    if (Objects.nonNull(field)) {
+                        map.put(columns.get(i), field.toString());
+                    }
+                } else {
+                    throw new IllegalArgumentException("RowData is not 
GenericData.");
+                }
+            }
+            rows.add(map);
+        }
+        return rows;
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/FlinkSqlStatementSetBuilder.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/FlinkSqlStatementSetBuilder.java
new file mode 100644
index 0000000..637de23
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/FlinkSqlStatementSetBuilder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.web.engine.flink.sql.gateway.utils;
+
+import org.apache.commons.collections.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * Utility class for building Flink SQL STATEMENT SET strings for batch 
execution.
+ *
+ * <p>This class provides a static method to construct a STATEMENT SET from a 
list of individual
+ * INSERT statements. If there is only one INSERT statement, it returns the 
statement as is. For
+ * multiple statements, it wraps them in a Flink SQL EXECUTE STATEMENT SET 
block.
+ */
+public class FlinkSqlStatementSetBuilder {
+
+    private static final String EXECUTE_STATEMENT_SET = "EXECUTE STATEMENT 
SET\n";
+    private static final String BEGIN_STATEMENT = "BEGIN\n";
+    private static final String END_STATEMENT = "END;\n";
+    private static final String STATEMENT_DELIMITER = ";\n";
+
+    public static String buildStatementSet(List<String> insertStatements) {
+        StringBuilder statementSetBuilder = new StringBuilder();
+
+        if (CollectionUtils.isNotEmpty(insertStatements)) {
+            if (insertStatements.size() > 1) {
+                statementSetBuilder.append(EXECUTE_STATEMENT_SET);
+                statementSetBuilder.append(BEGIN_STATEMENT);
+
+                for (String insertStatement : insertStatements) {
+                    statementSetBuilder.append(insertStatement);
+                    statementSetBuilder.append(STATEMENT_DELIMITER);
+                }
+
+                statementSetBuilder.append(END_STATEMENT);
+            } else {
+                statementSetBuilder.append(insertStatements.get(0));
+                statementSetBuilder.append(STATEMENT_DELIMITER);
+            }
+        }
+
+        return statementSetBuilder.toString();
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/SqlGateWayRestClient.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/SqlGateWayRestClient.java
new file mode 100644
index 0000000..4679b59
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/main/java/org/apache/paimon/web/engine/flink/sql/gateway/utils/SqlGateWayRestClient.java
@@ -0,0 +1,147 @@
+/*
+ * 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.web.engine.flink.sql.gateway.utils;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.rest.FileUpload;
+import org.apache.flink.runtime.rest.RestClient;
+import org.apache.flink.runtime.rest.messages.EmptyMessageParameters;
+import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
+import org.apache.flink.runtime.rest.messages.MessageHeaders;
+import org.apache.flink.runtime.rest.messages.MessageParameters;
+import org.apache.flink.runtime.rest.messages.RequestBody;
+import org.apache.flink.runtime.rest.messages.ResponseBody;
+import org.apache.flink.runtime.rest.versioning.RestAPIVersion;
+import org.apache.flink.util.ConfigurationException;
+import org.apache.flink.util.concurrent.Executors;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+/** SQL gateway rest client. */
+public class SqlGateWayRestClient extends RestClient {
+    private final String sqlGatewayHost;
+    private final int sqlGatewayPort;
+
+    public SqlGateWayRestClient(String sqlGatewayHost, int sqlGatewayPort)
+            throws ConfigurationException {
+        super(new Configuration(), Executors.newDirectExecutorService());
+        this.sqlGatewayHost = sqlGatewayHost;
+        this.sqlGatewayPort = sqlGatewayPort;
+    }
+
+    public <
+                    M extends MessageHeaders<R, P, U>,
+                    U extends MessageParameters,
+                    R extends RequestBody,
+                    P extends ResponseBody>
+            CompletableFuture<P> sendRequest(
+                    M messageHeaders,
+                    U messageParameters,
+                    R request,
+                    Collection<FileUpload> fileUploads,
+                    RestAPIVersion<? extends RestAPIVersion<?>> apiVersion) {
+        return throwExceptionHandler(
+                () -> {
+                    try {
+                        return super.sendRequest(
+                                sqlGatewayHost,
+                                sqlGatewayPort,
+                                messageHeaders,
+                                messageParameters,
+                                request,
+                                fileUploads,
+                                apiVersion);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    public <
+                    M extends MessageHeaders<R, P, U>,
+                    U extends MessageParameters,
+                    R extends RequestBody,
+                    P extends ResponseBody>
+            CompletableFuture<P> sendRequest(M messageHeaders, U 
messageParameters, R request) {
+        return throwExceptionHandler(
+                () -> {
+                    try {
+                        return super.sendRequest(
+                                sqlGatewayHost,
+                                sqlGatewayPort,
+                                messageHeaders,
+                                messageParameters,
+                                request);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    public <
+                    M extends MessageHeaders<R, P, U>,
+                    U extends MessageParameters,
+                    R extends RequestBody,
+                    P extends ResponseBody>
+            CompletableFuture<P> sendRequest(
+                    M messageHeaders,
+                    U messageParameters,
+                    R request,
+                    Collection<FileUpload> fileUploads) {
+        return throwExceptionHandler(
+                () -> {
+                    try {
+                        return super.sendRequest(
+                                sqlGatewayHost,
+                                sqlGatewayPort,
+                                messageHeaders,
+                                messageParameters,
+                                request,
+                                fileUploads);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    public <
+                    M extends MessageHeaders<EmptyRequestBody, P, 
EmptyMessageParameters>,
+                    P extends ResponseBody>
+            CompletableFuture<P> sendRequest(M messageHeaders) {
+        return throwExceptionHandler(
+                () -> {
+                    try {
+                        return super.sendRequest(sqlGatewayHost, 
sqlGatewayPort, messageHeaders);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    private <T> T throwExceptionHandler(Supplier<T> func0) {
+        try {
+            return func0.get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/TestBase.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/TestBase.java
new file mode 100644
index 0000000..32e3a3f
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/TestBase.java
@@ -0,0 +1,201 @@
+/*
+ * 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.web.engine.flink.sql.gataway;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.testutils.CommonTestUtils;
+import org.apache.flink.table.gateway.api.SqlGatewayService;
+import 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils;
+import org.apache.flink.table.gateway.rest.SqlGatewayRestEndpoint;
+import org.apache.flink.table.gateway.rest.util.SqlGatewayRestOptions;
+import org.apache.flink.table.gateway.service.SqlGatewayServiceImpl;
+import org.apache.flink.table.gateway.service.context.DefaultContext;
+import org.apache.flink.table.gateway.service.session.SessionManager;
+import org.apache.flink.test.junit5.MiniClusterExtension;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static 
org.apache.flink.configuration.ConfigConstants.ENV_FLINK_CONF_DIR;
+import static 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils.getEndpointConfig;
+import static 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils.getSqlGatewayOptionPrefix;
+import static 
org.apache.flink.table.gateway.rest.SqlGatewayRestEndpointFactory.IDENTIFIER;
+import static 
org.apache.flink.table.gateway.rest.SqlGatewayRestEndpointFactory.rebuildRestEndpointOptions;
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/** The base class for sql gateway test. */
+public class TestBase {
+
+    @RegisterExtension
+    @Order(1)
+    private static final MiniClusterExtension MINI_CLUSTER = new 
MiniClusterExtension();
+
+    @RegisterExtension
+    @Order(2)
+    protected static final SqlGatewayServiceExtension 
SQL_GATEWAY_SERVICE_EXTENSION =
+            new 
SqlGatewayServiceExtension(MINI_CLUSTER::getClientConfiguration);
+
+    @Nullable protected static String targetAddress = null;
+    @Nullable private static SqlGatewayRestEndpoint sqlGatewayRestEndpoint = 
null;
+
+    protected static int port = 0;
+
+    @BeforeAll
+    static void start() throws Exception {
+        final String address = 
InetAddress.getLoopbackAddress().getHostAddress();
+        Configuration config = getBaseConfig(getFlinkConfig(address, address, 
"0"));
+        sqlGatewayRestEndpoint =
+                new SqlGatewayRestEndpoint(config, 
SQL_GATEWAY_SERVICE_EXTENSION.getService());
+        sqlGatewayRestEndpoint.start();
+        InetSocketAddress serverAddress = 
checkNotNull(sqlGatewayRestEndpoint.getServerAddress());
+        targetAddress = serverAddress.getHostName();
+        port = serverAddress.getPort();
+    }
+
+    @AfterAll
+    static void stop() throws Exception {
+        checkNotNull(sqlGatewayRestEndpoint);
+        sqlGatewayRestEndpoint.close();
+    }
+
+    private static Configuration getBaseConfig(Configuration flinkConf) {
+        SqlGatewayEndpointFactoryUtils.DefaultEndpointFactoryContext context =
+                new 
SqlGatewayEndpointFactoryUtils.DefaultEndpointFactoryContext(
+                        null, flinkConf, getEndpointConfig(flinkConf, 
IDENTIFIER));
+
+        return rebuildRestEndpointOptions(context.getEndpointOptions());
+    }
+
+    private static Configuration getFlinkConfig(
+            String address, String bindAddress, String portRange) {
+        final Configuration config = new Configuration();
+        if (address != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.ADDRESS.key()), address);
+        }
+        if (bindAddress != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.BIND_ADDRESS.key()),
+                    bindAddress);
+        }
+        if (portRange != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.PORT.key()), portRange);
+        }
+        return config;
+    }
+
+    private static String getSqlGatewayRestOptionFullName(String key) {
+        return getSqlGatewayOptionPrefix(IDENTIFIER) + key;
+    }
+
+    /** A simple {@link Extension} to be used by tests that require a {@link 
SqlGatewayService}. */
+    static class SqlGatewayServiceExtension implements BeforeAllCallback, 
AfterAllCallback {
+
+        private SqlGatewayService service;
+        private SessionManager sessionManager;
+        private TemporaryFolder temporaryFolder;
+        private final Supplier<Configuration> configSupplier;
+        private final Function<DefaultContext, SessionManager> 
sessionManagerCreator;
+
+        public SqlGatewayServiceExtension(Supplier<Configuration> 
configSupplier) {
+            this(configSupplier, SessionManager::create);
+        }
+
+        public SqlGatewayServiceExtension(
+                Supplier<Configuration> configSupplier,
+                Function<DefaultContext, SessionManager> 
sessionManagerCreator) {
+            this.configSupplier = configSupplier;
+            this.sessionManagerCreator = sessionManagerCreator;
+        }
+
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            final Map<String, String> originalEnv = System.getenv();
+            try {
+                // prepare conf dir
+                temporaryFolder = new TemporaryFolder();
+                temporaryFolder.create();
+                File confFolder = temporaryFolder.newFolder("conf");
+                File confYaml = new File(confFolder, "flink-conf.yaml");
+                if (!confYaml.createNewFile()) {
+                    throw new IOException("Can't create testing 
flink-conf.yaml file.");
+                }
+
+                FileUtils.write(
+                        confYaml,
+                        getFlinkConfContent(configSupplier.get().toMap()),
+                        StandardCharsets.UTF_8);
+
+                // adjust the test environment for the purposes of this test
+                Map<String, String> map = new HashMap<>(System.getenv());
+                map.put(ENV_FLINK_CONF_DIR, confFolder.getAbsolutePath());
+                CommonTestUtils.setEnv(map);
+
+                sessionManager =
+                        sessionManagerCreator.apply(
+                                DefaultContext.load(
+                                        new Configuration(), 
Collections.emptyList(), true, false));
+            } finally {
+                CommonTestUtils.setEnv(originalEnv);
+            }
+
+            service = new SqlGatewayServiceImpl(sessionManager);
+            sessionManager.start();
+        }
+
+        @Override
+        public void afterAll(ExtensionContext context) throws Exception {
+            if (sessionManager != null) {
+                sessionManager.stop();
+            }
+            temporaryFolder.delete();
+        }
+
+        public SqlGatewayService getService() {
+            return service;
+        }
+
+        private String getFlinkConfContent(Map<String, String> flinkConf) {
+            StringBuilder sb = new StringBuilder();
+            flinkConf.forEach((k, v) -> sb.append(k).append(": 
").append(v).append("\n"));
+            return sb.toString();
+        }
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/client/SqlGatewayClientTest.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/client/SqlGatewayClientTest.java
new file mode 100644
index 0000000..1b20e31
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/client/SqlGatewayClientTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.web.engine.flink.sql.gataway.client;
+
+import org.apache.paimon.web.engine.flink.sql.gataway.TestBase;
+import org.apache.paimon.web.engine.flink.sql.gateway.client.SqlGatewayClient;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+
+import org.apache.commons.collections.MapUtils;
+import 
org.apache.flink.table.gateway.rest.message.statement.FetchResultsResponseBody;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Test for {@link SqlGatewayClient}. */
+public class SqlGatewayClientTest extends TestBase {
+
+    SqlGatewayClient client;
+    SessionEntity session;
+
+    private static final String SESSION_NAME = "test_session";
+
+    @BeforeEach
+    void before() throws Exception {
+        client = new SqlGatewayClient(targetAddress, port);
+        session = client.openSession(SESSION_NAME);
+    }
+
+    @Test
+    public void testGetSessionConfig() throws Exception {
+        Map<String, String> sessionConfig = 
client.getSessionConfig(session.getSessionId());
+        assertTrue(MapUtils.isNotEmpty(sessionConfig));
+    }
+
+    @Test
+    public void testCloseSession() throws Exception {
+        String status = client.closeSession(session.getSessionId());
+        assertEquals("CLOSED", status);
+    }
+
+    @Test
+    public void testExecuteStatement() throws Exception {
+        String operationHandle = 
client.executeStatement(session.getSessionId(), "SELECT 1", null);
+        assertNotNull(operationHandle);
+    }
+
+    @Test
+    public void testCompleteStatementHints() throws Exception {
+        List<String> list = 
client.completeStatementHints(session.getSessionId(), "CREATE TA");
+        assertFalse(list.isEmpty());
+    }
+
+    @Test
+    public void testFetchResults() throws Exception {
+        String operationHandle = 
client.executeStatement(session.getSessionId(), "SELECT 1", null);
+        FetchResultsResponseBody fetchResultsResponseBody =
+                client.fetchResults(session.getSessionId(), operationHandle, 
0);
+        assertNotNull(fetchResultsResponseBody);
+        assertEquals("PAYLOAD", 
fetchResultsResponseBody.getResultType().name());
+        FetchResultsResponseBody fetchResultsResponseBodyNext =
+                client.fetchResults(session.getSessionId(), operationHandle, 
1);
+        assertNotNull(fetchResultsResponseBodyNext);
+        assertEquals("EOS", 
fetchResultsResponseBodyNext.getResultType().name());
+    }
+
+    @Test
+    public void testGetOperationStatus() throws Exception {
+        String operationHandle = 
client.executeStatement(session.getSessionId(), "SELECT 1", null);
+        String operationStatus = 
client.getOperationStatus(session.getSessionId(), operationHandle);
+        assertNotNull(operationStatus);
+    }
+
+    @Test
+    public void testCancelOperation() throws Exception {
+        String operationHandle = 
client.executeStatement(session.getSessionId(), "SELECT 1", null);
+        String status = client.cancelOperation(session.getSessionId(), 
operationHandle);
+        assertEquals("CANCELED", status);
+    }
+
+    @Test
+    public void testCloseOperation() throws Exception {
+        String operationHandle = 
client.executeStatement(session.getSessionId(), "SELECT 1", null);
+        String status = client.closeOperation(session.getSessionId(), 
operationHandle);
+        assertEquals("CLOSED", status);
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/FlinkSqlGatewayExecutorTest.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/FlinkSqlGatewayExecutorTest.java
new file mode 100644
index 0000000..42620af
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/FlinkSqlGatewayExecutorTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.web.engine.flink.sql.gataway.executor;
+
+import org.apache.paimon.web.engine.flink.common.result.ExecutionResult;
+import org.apache.paimon.web.engine.flink.common.result.FetchResultParams;
+import org.apache.paimon.web.engine.flink.sql.gataway.TestBase;
+import org.apache.paimon.web.engine.flink.sql.gateway.client.SqlGatewayClient;
+import 
org.apache.paimon.web.engine.flink.sql.gateway.executor.FlinkSqlGatewayExecutor;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Test for {@link FlinkSqlGatewayExecutor}. */
+public class FlinkSqlGatewayExecutorTest extends TestBase {
+
+    SqlGatewayClient client;
+    SessionEntity session;
+    FlinkSqlGatewayExecutor executor;
+
+    private static final String SESSION_NAME = "test_session";
+
+    @BeforeEach
+    void before() throws Exception {
+        client = new SqlGatewayClient(targetAddress, port);
+        session = client.openSession(SESSION_NAME);
+        executor = new FlinkSqlGatewayExecutor(session);
+    }
+
+    @Test
+    public void testExecuteSql() throws Exception {
+        ExecutionResult executionResult = 
executor.executeSql(StatementsConstant.statement);
+        assertNotNull(executionResult);
+        assertNotNull(executionResult.getJobId());
+    }
+
+    @Test
+    public void testExecuteStatementSetSql() throws Exception {
+        ExecutionResult executionResult = 
executor.executeSql(StatementsConstant.statementSetSql);
+        assertNotNull(executionResult);
+        assertNotNull(executionResult.getJobId());
+    }
+
+    @Test
+    public void testExecutorStatementWithoutResult() throws Exception {
+        ExecutionResult executionResult = 
executor.executeSql(StatementsConstant.createStatement);
+        assertNull(executionResult);
+    }
+
+    @Test
+    public void testExecuteDQLStatementWithPendingInsertStatements() {
+        Exception exception =
+                assertThrows(
+                        UnsupportedOperationException.class,
+                        () -> {
+                            executor.executeSql(
+                                    
StatementsConstant.selectStatementWithPendingInsertStatements);
+                        });
+        String expectedMessage = "Cannot execute DQL statement with pending 
INSERT statements.";
+        String actualMessage = exception.getMessage();
+        assertTrue(actualMessage.contains(expectedMessage));
+    }
+
+    @Test
+    public void testExecuteBadSqlStatement() {
+        Exception exception =
+                assertThrows(
+                        UnsupportedOperationException.class,
+                        () -> {
+                            
executor.executeSql(StatementsConstant.badStatement);
+                        });
+        String expectedMessage = "Unsupported operation type: CREAT";
+        String actualMessage = exception.getMessage();
+        assertTrue(actualMessage.contains(expectedMessage));
+    }
+
+    @Test
+    public void testFetchResults() throws Exception {
+        ExecutionResult executionResult = 
executor.executeSql(StatementsConstant.selectStatement);
+        assertNotNull(executionResult);
+        assertNotNull(executionResult.getJobId());
+        assertNotNull(executionResult.getSubmitId());
+        assertTrue(executionResult.shouldFetchResult());
+        FetchResultParams params =
+                FetchResultParams.builder()
+                        .sessionId(session.getSessionId())
+                        .submitId(executionResult.getSubmitId())
+                        .token(1L)
+                        .build();
+        ExecutionResult fetchResult = executor.fetchResults(params);
+        assertFalse(fetchResult.getData().isEmpty());
+    }
+}
diff --git 
a/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/StatementsConstant.java
 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/StatementsConstant.java
new file mode 100644
index 0000000..c08f05a
--- /dev/null
+++ 
b/paimon-web-engine/paimon-web-engine-flink/paimon-web-engine-flink-sql-gateway/src/test/java/org/apache/paimon/web/engine/flink/sql/gataway/executor/StatementsConstant.java
@@ -0,0 +1,196 @@
+/*
+ * 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.web.engine.flink.sql.gataway.executor;
+
+/** Statements constant. */
+public class StatementsConstant {
+
+    public static String statement =
+            "CREATE TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n"
+                    + "CREATE TABLE IF NOT EXISTS sink_table(\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` TIMESTAMP(3),\n"
+                    + "    `one_minute_sum` BIGINT\n"
+                    + ") WITH('connector' = 'print');\n"
+                    + "\n"
+                    + "INSERT INTO\n"
+                    + "    sink_table\n"
+                    + "SELECT\n"
+                    + "    product,\n"
+                    + "    amount,\n"
+                    + "    order_time,\n"
+                    + "    0 as one_minute_sum\n"
+                    + "FROM\n"
+                    + "    t_order;";
+
+    public static String selectStatement =
+            "CREATE TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n"
+                    + "SELECT * FROM t_order;";
+
+    public static String selectStatementWithPendingInsertStatements =
+            "CREATE TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n"
+                    + "CREATE TABLE IF NOT EXISTS sink_table(\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` TIMESTAMP(3),\n"
+                    + "    `one_minute_sum` BIGINT\n"
+                    + ") WITH('connector' = 'print');\n"
+                    + "\n"
+                    + "INSERT INTO\n"
+                    + "    sink_table\n"
+                    + "SELECT\n"
+                    + "    product,\n"
+                    + "    amount,\n"
+                    + "    order_time,\n"
+                    + "    0 as one_minute_sum\n"
+                    + "FROM\n"
+                    + "    t_order;\n"
+                    + "SELECT * FROM t_order LIMIT 10;";
+
+    public static String createStatement =
+            "CREATE TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n";
+
+    public static String badStatement =
+            "CREAT TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n";
+
+    public static String statementSetSql =
+            "CREATE TABLE IF NOT EXISTS t_order(\n"
+                    + "    `order_id` BIGINT,\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` as CAST(CURRENT_TIMESTAMP AS 
TIMESTAMP(3)),\n"
+                    + "    WATERMARK FOR order_time AS order_time-INTERVAL '2' 
SECOND\n"
+                    + ") WITH(\n"
+                    + "    'connector' = 'datagen',\n"
+                    + "    'rows-per-second' = '1',\n"
+                    + "    'fields.order_id.min' = '1',\n"
+                    + "    'fields.order_id.max' = '2',\n"
+                    + "    'fields.amount.min' = '1',\n"
+                    + "    'fields.amount.max' = '10',\n"
+                    + "    'fields.product.min' = '1',\n"
+                    + "    'fields.product.max' = '2'\n"
+                    + ");\n"
+                    + "CREATE TABLE IF NOT EXISTS sink_table_01(\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` TIMESTAMP(3),\n"
+                    + "    `one_minute_sum` BIGINT\n"
+                    + ") WITH('connector' = 'print');\n"
+                    + "CREATE TABLE IF NOT EXISTS sink_table_02(\n"
+                    + "    `product` BIGINT,\n"
+                    + "    `amount` BIGINT,\n"
+                    + "    `order_time` TIMESTAMP(3),\n"
+                    + "    `one_minute_sum` BIGINT\n"
+                    + ") WITH('connector' = 'print');\n"
+                    + "\n"
+                    + "INSERT INTO\n"
+                    + "    sink_table_01\n"
+                    + "SELECT\n"
+                    + "    product,\n"
+                    + "    amount,\n"
+                    + "    order_time,\n"
+                    + "    0 as one_minute_sum\n"
+                    + "FROM\n"
+                    + "    t_order;\n"
+                    + "INSERT INTO\n"
+                    + "    sink_table_02\n"
+                    + "SELECT\n"
+                    + "    product,\n"
+                    + "    amount,\n"
+                    + "    order_time,\n"
+                    + "    0 as one_minute_sum\n"
+                    + "FROM\n"
+                    + "    t_order;";
+}
diff --git a/paimon-web-engine/paimon-web-engine-flink/pom.xml 
b/paimon-web-engine/paimon-web-engine-flink/pom.xml
index fa44e1c..9365cff 100644
--- a/paimon-web-engine/paimon-web-engine-flink/pom.xml
+++ b/paimon-web-engine/paimon-web-engine-flink/pom.xml
@@ -35,6 +35,7 @@ under the License.
 
     <modules>
         <module>paimon-web-engine-flink-common</module>
+        <module>paimon-web-engine-flink-sql-gateway</module>
     </modules>
 
     <properties>
diff --git a/paimon-web-engine/pom.xml b/paimon-web-engine/pom.xml
index 81a45f5..1070298 100644
--- a/paimon-web-engine/pom.xml
+++ b/paimon-web-engine/pom.xml
@@ -36,4 +36,12 @@ under the License.
     <modules>
         <module>paimon-web-engine-flink</module>
     </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.paimon</groupId>
+            <artifactId>paimon-web-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
 </project>
\ No newline at end of file

Reply via email to