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

apkhmv pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 35d373c931f IGNITE-27452 Add --timed option to CLI (#7314)
35d373c931f is described below

commit 35d373c931fc7ec4b6ffe461896f70997a6eadc3
Author: Aleksandr Pakhomov <[email protected]>
AuthorDate: Fri Dec 26 14:37:45 2025 +0300

    IGNITE-27452 Add --timed option to CLI (#7314)
---
 .../cli/commands/sql/ItSqlTimedOptionTest.java     | 134 +++++++++++++++++++++
 .../ignite/internal/cli/commands/Options.java      |   7 ++
 .../internal/cli/commands/sql/SqlCommand.java      |  54 ++++++++-
 .../internal/cli/commands/sql/SqlExecCommand.java  |   7 +-
 .../cli/commands/sql/SqlExecReplCommand.java       |   7 +-
 .../internal/cli/commands/sql/SqlReplCommand.java  |  53 +++++++-
 .../cli/decorators/SqlQueryResultDecorator.java    |   8 +-
 .../apache/ignite/internal/cli/sql/SqlManager.java |   2 +
 .../ignite/internal/cli/sql/SqlQueryResult.java    |  37 +++++-
 .../internal/cli/commands/sql/SqlCommandTest.java  |  12 ++
 10 files changed, 309 insertions(+), 12 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlTimedOptionTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlTimedOptionTest.java
new file mode 100644
index 00000000000..6b0dd7a0175
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlTimedOptionTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ignite.internal.cli.commands.sql;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the --timed option of the sql command.
+ */
+public class ItSqlTimedOptionTest extends CliSqlCommandTestBase {
+
+    /**
+     * Tests that --timed option displays query execution time after SELECT 
result.
+     */
+    @Test
+    void timedSelectQuery() {
+        execute("sql", "select * from person", "--jdbc-url", JDBC_URL, 
"--timed");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                // Should contain the table with data
+                () -> assertOutputContains("ID"),
+                () -> assertOutputContains("NAME"),
+                // Should contain timing information at the end
+                () -> assertOutputMatches("(?s).*Query executed in \\d+ms 
\\(client-side\\)\\.\\s*"),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    /**
+     * Tests that --timed option displays query execution time after INSERT 
result.
+     */
+    @Test
+    void timedInsertQuery() {
+        execute("sql", "insert into person(id, name, salary) values (100, 
'Test', 50.0)", "--jdbc-url", JDBC_URL, "--timed");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                // Should contain the update count message
+                () -> assertOutputContains("Updated 1 rows."),
+                // Should contain timing information at the end
+                () -> assertOutputMatches("(?s).*Query executed in \\d+ms 
\\(client-side\\)\\.\\s*"),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    /**
+     * Tests that --timed option displays query execution time after multiple 
statements.
+     */
+    @Test
+    void timedMultipleStatements() {
+        execute("sql", "insert into person(id, name, salary) values (101, 'A', 
10.0); select count(*) from person",
+                "--jdbc-url", JDBC_URL, "--timed");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                // Should contain insert result
+                () -> assertOutputContains("Updated 1 rows."),
+                // Should contain select result (column name)
+                () -> assertOutputContains("COUNT(*)"),
+                // Should contain timing information at the very end (only 
once, after all results)
+                () -> assertOutputMatches("(?s).*Query executed in \\d+ms 
\\(client-side\\)\\.\\s*"),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    /**
+     * Tests that without --timed option, no timing information is displayed.
+     */
+    @Test
+    void noTimedOption() {
+        execute("sql", "select * from person", "--jdbc-url", JDBC_URL);
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputContains("ID"),
+                () -> assertOutputDoesNotContain("Query executed in"),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    /**
+     * Tests that --timed works together with --plain option.
+     */
+    @Test
+    void timedWithPlainOption() {
+        execute("sql", "select id from person where id = 0", "--jdbc-url", 
JDBC_URL, "--timed", "--plain");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                // Plain output should not have fancy borders
+                () -> assertOutputDoesNotContain("╔"),
+                () -> assertOutputDoesNotContain("║"),
+                // Should still contain timing information
+                () -> assertOutputMatches("(?s).*Query executed in \\d+ms 
\\(client-side\\)\\.\\s*"),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    /**
+     * Tests that --timed shows time for DDL queries (CREATE TABLE).
+     */
+    @Test
+    void timedDdlQuery() {
+        execute("sql", "create table timed_test(id int primary key)", 
"--jdbc-url", JDBC_URL, "--timed");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputContains("Updated 0 rows."),
+                () -> assertOutputMatches("(?s).*Query executed in \\d+ms 
\\(client-side\\)\\.\\s*"),
+                this::assertErrOutputIsEmpty
+        );
+
+        // Clean up the created table
+        execute("sql", "drop table timed_test", "--jdbc-url", JDBC_URL);
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
index 3445a9f864d..29db05f2a4f 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
@@ -54,6 +54,7 @@ public enum Options {
     UNIT_NODES(Constants.UNIT_NODES_OPTION, Constants.UNIT_NODES_OPTION, 
Constants.UNIT_NODES_OPTION_DESC),
 
     PLAIN(Constants.PLAIN_OPTION, Constants.PLAIN_OPTION, 
Constants.PLAIN_OPTION_DESC),
+    TIMED(Constants.TIMED_OPTION, Constants.TIMED_OPTION, 
Constants.TIMED_OPTION_DESC),
     VERBOSE(Constants.VERBOSE_OPTION, Constants.VERBOSE_OPTION_SHORT, 
Constants.VERBOSE_OPTION_DESC),
     HELP(Constants.HELP_OPTION, Constants.HELP_OPTION_SHORT, 
Constants.HELP_OPTION_DESC),
     VERSION(Constants.VERSION_OPTION, Constants.VERSION_OPTION, 
Constants.VERSION_OPTION_DESC),
@@ -204,6 +205,12 @@ public enum Options {
         public static final String PLAIN_OPTION_DESC = "Display output with 
plain formatting. "
                 + "Might be useful if you want to pipe the output to another 
command";
 
+        /** Timed option long name. */
+        public static final String TIMED_OPTION = "--timed";
+
+        /** Timed option description. */
+        public static final String TIMED_OPTION_DESC = "Display query 
execution time (measured on the client) after the output";
+
         /** JDBC URL option long name. */
         public static final String JDBC_URL_OPTION = "--jdbc-url";
 
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
index 9ab52fdd34b..918130c18a2 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
@@ -17,13 +17,26 @@
 
 package org.apache.ignite.internal.cli.commands.sql;
 
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_KEY;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION_DESC;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.sql.planner.SqlPlannerCommand;
-import org.apache.ignite.internal.util.ArrayUtils;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.IFactory;
+import picocli.CommandLine.Option;
 import picocli.CommandLine.Unmatched;
 
 /**
@@ -40,6 +53,21 @@ import picocli.CommandLine.Unmatched;
         description = "SQL query engine operations."
 )
 public class SqlCommand extends BaseCommand implements Callable<Integer> {
+    // These options are documented here for --help display but are actually 
processed by SqlExecCommand.
+    // All args are passed through to SqlExecCommand via @Unmatched.
+
+    @Option(names = JDBC_URL_OPTION, descriptionKey = JDBC_URL_KEY, 
description = JDBC_URL_OPTION_DESC)
+    private String jdbc;
+
+    @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
+    private boolean plain;
+
+    @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
+    private boolean timed;
+
+    @Option(names = SCRIPT_FILE_OPTION, description = SCRIPT_FILE_OPTION_DESC)
+    private String file;
+
     @Unmatched
     private String[] args;
 
@@ -58,6 +86,28 @@ public class SqlCommand extends BaseCommand implements 
Callable<Integer> {
                 .setDefaultValueProvider(spec.defaultValueProvider())
                 
.setExecutionExceptionHandler(spec.commandLine().getExecutionExceptionHandler());
 
-        return commandLine.execute(args == null ?  
ArrayUtils.STRING_EMPTY_ARRAY : args);
+        return commandLine.execute(buildArgs());
+    }
+
+    private String[] buildArgs() {
+        List<String> result = new ArrayList<>();
+        // Add unmatched args first - they may contain positional parameters 
that need to be
+        // parsed before options for proper ArgGroup mutual exclusion 
detection in SqlExecCommand.
+        if (args != null) {
+            Collections.addAll(result, args);
+        }
+        if (jdbc != null) {
+            result.add(JDBC_URL_OPTION + "=" + jdbc);
+        }
+        if (plain) {
+            result.add(PLAIN_OPTION);
+        }
+        if (timed) {
+            result.add(TIMED_OPTION);
+        }
+        if (file != null) {
+            result.add(SCRIPT_FILE_OPTION + "=" + file);
+        }
+        return result.toArray(new String[0]);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
index 9f0144497bf..c8ca189a8dc 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
@@ -24,6 +24,8 @@ import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OP
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION_DESC;
 
 import java.io.File;
 import java.io.IOException;
@@ -56,6 +58,9 @@ public class SqlExecCommand extends BaseCommand implements 
Callable<Integer> {
     @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
     private boolean plain;
 
+    @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
+    private boolean timed;
+
     @ArgGroup(multiplicity = "1")
     private ExecOptions execOptions;
 
@@ -83,7 +88,7 @@ public class SqlExecCommand extends BaseCommand implements 
Callable<Integer> {
             return runPipeline(CallExecutionPipeline.builder(new 
SqlQueryCall(sqlManager))
                     .inputProvider(() -> new StringCallInput(executeCommand))
                     .exceptionHandler(SqlExceptionHandler.INSTANCE)
-                    .decorator(new SqlQueryResultDecorator(plain))
+                    .decorator(new SqlQueryResultDecorator(plain, timed))
             );
         } catch (SQLException e) {
             ExceptionWriter exceptionWriter = 
ExceptionWriter.fromPrintWriter(spec.commandLine().getErr());
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
index ca7214eecc4..34588cb5dac 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
@@ -24,6 +24,8 @@ import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OP
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION_DESC;
 import static 
org.apache.ignite.internal.cli.commands.treesitter.parser.Parser.isTreeSitterParserAvailable;
 import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.ansi;
 import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.fg;
@@ -88,6 +90,9 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
     @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
     private boolean plain;
 
+    @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
+    private boolean timed;
+
     @ArgGroup
     private ExecOptions execOptions;
 
@@ -218,7 +223,7 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
                 .inputProvider(() -> new StringCallInput(line))
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .decorator(new SqlQueryResultDecorator(plain))
+                .decorator(new SqlQueryResultDecorator(plain, timed))
                 .verbose(verbose)
                 .exceptionHandler(SqlExceptionHandler.INSTANCE)
                 .build();
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
index 539a475894a..4dedb459109 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
@@ -17,13 +17,26 @@
 
 package org.apache.ignite.internal.cli.commands.sql;
 
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_KEY;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.TIMED_OPTION_DESC;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import 
org.apache.ignite.internal.cli.commands.sql.planner.SqlPlannerReplCommand;
-import org.apache.ignite.internal.util.ArrayUtils;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.IFactory;
+import picocli.CommandLine.Option;
 import picocli.CommandLine.Unmatched;
 
 /**
@@ -40,6 +53,21 @@ import picocli.CommandLine.Unmatched;
         description = "SQL query engine operations."
 )
 public class SqlReplCommand extends BaseCommand implements Callable<Integer> {
+    // These options are documented here for --help display but are actually 
processed by SqlExecReplCommand.
+    // All args are passed through to SqlExecReplCommand via @Unmatched.
+
+    @Option(names = JDBC_URL_OPTION, descriptionKey = JDBC_URL_KEY, 
description = JDBC_URL_OPTION_DESC)
+    private String jdbc;
+
+    @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
+    private boolean plain;
+
+    @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
+    private boolean timed;
+
+    @Option(names = SCRIPT_FILE_OPTION, description = SCRIPT_FILE_OPTION_DESC)
+    private String file;
+
     @Unmatched
     private String[] args;
 
@@ -58,6 +86,27 @@ public class SqlReplCommand extends BaseCommand implements 
Callable<Integer> {
                 .setDefaultValueProvider(spec.defaultValueProvider())
                 
.setExecutionExceptionHandler(spec.commandLine().getExecutionExceptionHandler());
 
-        return commandLine.execute(args == null ?  
ArrayUtils.STRING_EMPTY_ARRAY : args);
+        return commandLine.execute(buildArgs());
+    }
+
+    private String[] buildArgs() {
+        List<String> result = new ArrayList<>();
+        // Add unmatched args first to preserve positional parameter order.
+        if (args != null) {
+            Collections.addAll(result, args);
+        }
+        if (jdbc != null) {
+            result.add(JDBC_URL_OPTION + "=" + jdbc);
+        }
+        if (plain) {
+            result.add(PLAIN_OPTION);
+        }
+        if (timed) {
+            result.add(TIMED_OPTION);
+        }
+        if (file != null) {
+            result.add(SCRIPT_FILE_OPTION + "=" + file);
+        }
+        return result.toArray(new String[0]);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/SqlQueryResultDecorator.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/SqlQueryResultDecorator.java
index 77c4aacd001..2c99e6a2ea8 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/SqlQueryResultDecorator.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/SqlQueryResultDecorator.java
@@ -26,13 +26,19 @@ import org.apache.ignite.internal.cli.sql.SqlQueryResult;
  */
 public class SqlQueryResultDecorator implements Decorator<SqlQueryResult, 
TerminalOutput> {
     private final boolean plain;
+    private final boolean timed;
 
     public SqlQueryResultDecorator(boolean plain) {
+        this(plain, false);
+    }
+
+    public SqlQueryResultDecorator(boolean plain, boolean timed) {
         this.plain = plain;
+        this.timed = timed;
     }
 
     @Override
     public TerminalOutput decorate(SqlQueryResult data) {
-        return data.getResult(plain);
+        return data.getResult(plain, timed);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlManager.java 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlManager.java
index 372957dcae5..0a9989066b9 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlManager.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlManager.java
@@ -50,6 +50,7 @@ public class SqlManager implements AutoCloseable {
 
         SqlQueryResultBuilder sqlQueryResultBuilder = new 
SqlQueryResultBuilder();
 
+        long startTime = System.currentTimeMillis();
         try (Statement statement = connection.createStatement()) {
             statement.execute(sql);
 
@@ -63,6 +64,7 @@ public class SqlManager implements AutoCloseable {
                 }
             } while (statement.getMoreResults() || statement.getUpdateCount() 
!= -1);
 
+            sqlQueryResultBuilder.setDurationMs(System.currentTimeMillis() - 
startTime);
             return sqlQueryResultBuilder.build();
         }
     }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResult.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResult.java
index 4f24efff54f..1aaa10a8106 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResult.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResult.java
@@ -28,9 +28,30 @@ import org.apache.ignite.internal.cli.sql.table.Table;
  */
 public class SqlQueryResult {
     private final List<SqlQueryResultItem> sqlQueryResultItems;
+    private final long durationMs;
 
-    private SqlQueryResult(List<SqlQueryResultItem> sqlQueryResultItems) {
+    private SqlQueryResult(List<SqlQueryResultItem> sqlQueryResultItems, long 
durationMs) {
         this.sqlQueryResultItems = sqlQueryResultItems;
+        this.durationMs = durationMs;
+    }
+
+    /**
+     * SQL query result provider.
+     *
+     * @param plain Whether to use plain formatting.
+     * @param timed Whether to include execution time in output.
+     * @return terminal output all items in query result.
+     */
+    public TerminalOutput getResult(boolean plain, boolean timed) {
+        return () -> {
+            String result = sqlQueryResultItems.stream()
+                    .map(x -> x.decorate(plain).toTerminalString())
+                    .collect(Collectors.joining(""));
+            if (timed) {
+                result += "Query executed in " + durationMs + "ms 
(client-side).\n";
+            }
+            return result;
+        };
     }
 
     /**
@@ -39,9 +60,7 @@ public class SqlQueryResult {
      * @return terminal output all items in query result.
      */
     public TerminalOutput getResult(boolean plain) {
-        return () -> sqlQueryResultItems.stream()
-            .map(x -> x.decorate(plain).toTerminalString())
-            .collect(Collectors.joining(""));
+        return getResult(plain, false);
     }
 
     /**
@@ -49,6 +68,7 @@ public class SqlQueryResult {
      */
     static class SqlQueryResultBuilder {
         private final List<SqlQueryResultItem> sqlQueryResultItems = new 
ArrayList<>();
+        private long durationMs;
 
         /**
          * Add table to query result.
@@ -64,8 +84,15 @@ public class SqlQueryResult {
             sqlQueryResultItems.add(new SqlQueryResultMessage(message + "\n"));
         }
 
+        /**
+         * Set the query execution duration in milliseconds.
+         */
+        void setDurationMs(long durationMs) {
+            this.durationMs = durationMs;
+        }
+
         public SqlQueryResult build() {
-            return new SqlQueryResult(sqlQueryResultItems);
+            return new SqlQueryResult(sqlQueryResultItems, durationMs);
         }
     }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
index 22602dc1de3..851d1ed1c74 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
@@ -56,4 +56,16 @@ public class SqlCommandTest extends CliCommandTestBase {
                 () -> assertErrOutputContains("<command>, --file=<file> are 
mutually exclusive (specify only one)")
         );
     }
+
+    @Test
+    @DisplayName("Should show --timed option in help")
+    void timedOptionInHelp() {
+        execute("--help");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputContains("--timed"),
+                () -> assertOutputContains("Display query execution time")
+        );
+    }
 }

Reply via email to