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 26debd34466 IGNITE-27515 Truncate table columns to fit terminal width 
in CLI REPL (#7420)
26debd34466 is described below

commit 26debd344663226135905e5fbad68205b6cf2d84
Author: Aleksandr Pakhomov <[email protected]>
AuthorDate: Fri Jan 23 01:08:19 2026 +0300

    IGNITE-27515 Truncate table columns to fit terminal width in CLI REPL 
(#7420)
    
    Co-authored-by: Claude Opus 4.5 <[email protected]>
---
 CLAUDE.md                                          |   8 +
 .../ignite/internal/cli/commands/Options.java      |  12 +
 .../cli/commands/sql/SqlExecReplCommand.java       |  25 +-
 .../internal/cli/commands/sql/SqlReplCommand.java  |  16 +
 .../ignite/internal/cli/config/CliConfigKeys.java  |  12 +-
 .../cli/decorators/SqlQueryResultDecorator.java    |  17 +-
 .../internal/cli/decorators/TableDecorator.java    |  35 +-
 .../internal/cli/decorators/TruncationConfig.java  | 180 ++++++++++
 .../ignite/internal/cli/sql/SqlQueryResult.java    |  31 +-
 .../internal/cli/sql/SqlQueryResultItem.java       |  15 +-
 .../internal/cli/sql/SqlQueryResultMessage.java    |   4 +-
 .../internal/cli/sql/SqlQueryResultTable.java      |   5 +-
 .../ignite/internal/cli/util/TableTruncator.java   | 266 +++++++++++++++
 .../internal/cli/util/TableTruncatorTest.java      | 375 +++++++++++++++++++++
 14 files changed, 978 insertions(+), 23 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 972fe4bc91b..b0b6ad8d27e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,6 +113,14 @@ idea inspect . 
.idea/inspectionProfiles/Project_Default.xml /tmp/results -d modu
 Notes:
 - IntelliJ IDEA must be closed for command-line inspections to work.
 - If `idea` command is not available, ask the user to install it via: **Tools 
> Create Command-line Launcher** in IntelliJ IDEA.
+- Check results: `find /tmp/results -name "*.xml" ! -name ".descriptions.xml" 
-exec cat {} \;`
+
+### Before Every PR (MANDATORY)
+Run IDEA inspections on the modified module(s) and fix all issues:
+```bash
+idea inspect . .idea/inspectionProfiles/Project_Default.xml /tmp/results -d 
modules/<module>
+```
+**IMPORTANT**: Always run IDEA inspections before creating or updating a PR. 
The inspections catch issues that checkstyle/PMD miss (e.g., methods that can 
be static, redundant suppressions, etc.).
 
 ### Git Push
 **Never use `git push` without specifying the target branch.** Always push 
explicitly:
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 8bd07db2c07..03b1e7de441 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
@@ -348,5 +348,17 @@ public enum Options {
 
         public static final String RESET_DATA_NODES_ZONE_NAMES_OPTION_DESC = 
"Comma-separated list of zone names to reset data nodes for. "
                 + "If not specified, resets for all zones.";
+
+        /** Maximum column width option long name. */
+        public static final String MAX_COL_WIDTH_OPTION = "--max-col-width";
+
+        /** Maximum column width option description. */
+        public static final String MAX_COL_WIDTH_OPTION_DESC = "Maximum column 
width for table output (default: 50)";
+
+        /** No truncate option long name. */
+        public static final String NO_TRUNCATE_OPTION = "--no-truncate";
+
+        /** No truncate option description. */
+        public static final String NO_TRUNCATE_OPTION_DESC = "Disable column 
truncation, show full content";
     }
 }
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 a667b747eb7..c7c7e2f6cbe 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
@@ -20,6 +20,10 @@ 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.MAX_COL_WIDTH_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.MAX_COL_WIDTH_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.NO_TRUNCATE_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.NO_TRUNCATE_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;
@@ -60,6 +64,7 @@ import 
org.apache.ignite.internal.cli.core.repl.executor.ReplExecutorProvider;
 import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
 import org.apache.ignite.internal.cli.core.style.AnsiStringSupport.Color;
 import org.apache.ignite.internal.cli.decorators.SqlQueryResultDecorator;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
 import org.apache.ignite.internal.cli.sql.SqlManager;
 import org.apache.ignite.internal.cli.sql.SqlSchemaProvider;
 import org.apache.ignite.internal.util.StringUtils;
@@ -74,6 +79,7 @@ import org.jline.reader.SyntaxError;
 import org.jline.reader.impl.DefaultHighlighter;
 import org.jline.reader.impl.DefaultParser;
 import org.jline.reader.impl.completer.AggregateCompleter;
+import org.jline.terminal.Terminal;
 import org.jline.utils.AttributedString;
 import picocli.CommandLine.ArgGroup;
 import picocli.CommandLine.Command;
@@ -94,6 +100,12 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
     @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
     private boolean timed;
 
+    @Option(names = MAX_COL_WIDTH_OPTION, description = 
MAX_COL_WIDTH_OPTION_DESC)
+    private Integer maxColWidth;
+
+    @Option(names = NO_TRUNCATE_OPTION, description = NO_TRUNCATE_OPTION_DESC)
+    private boolean noTruncate;
+
     @ArgGroup
     private ExecOptions execOptions;
 
@@ -117,6 +129,9 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
     @Inject
     private ApiClientFactory clientFactory;
 
+    @Inject
+    private Terminal terminal;
+
     private static String extract(File file) {
         try {
             return String.join("\n", Files.readAllLines(file.toPath(), 
StandardCharsets.UTF_8));
@@ -220,6 +235,14 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
     }
 
     private CallExecutionPipeline<?, ?> createSqlExecPipeline(SqlManager 
sqlManager, String line) {
+        TruncationConfig truncationConfig = TruncationConfig.fromConfig(
+                configManagerProvider,
+                terminal::getWidth,
+                maxColWidth,
+                noTruncate,
+                plain
+        );
+
         // Use CommandLineContextProvider to get the current REPL's output 
writer,
         // not the outer command's writer. This ensures SQL output goes through
         // the nested REPL's output capture for proper pager support.
@@ -227,7 +250,7 @@ public class SqlExecReplCommand extends BaseCommand 
implements Runnable {
                 .inputProvider(() -> new StringCallInput(line))
                 .output(CommandLineContextProvider.getContext().out())
                 .errOutput(CommandLineContextProvider.getContext().err())
-                .decorator(new SqlQueryResultDecorator(plain, timed))
+                .decorator(new SqlQueryResultDecorator(plain, timed, 
truncationConfig))
                 .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 4dedb459109..700f5806ed3 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
@@ -20,6 +20,10 @@ 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.MAX_COL_WIDTH_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.MAX_COL_WIDTH_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.NO_TRUNCATE_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.NO_TRUNCATE_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;
@@ -65,6 +69,12 @@ public class SqlReplCommand extends BaseCommand implements 
Callable<Integer> {
     @Option(names = TIMED_OPTION, description = TIMED_OPTION_DESC)
     private boolean timed;
 
+    @Option(names = MAX_COL_WIDTH_OPTION, description = 
MAX_COL_WIDTH_OPTION_DESC)
+    private Integer maxColWidth;
+
+    @Option(names = NO_TRUNCATE_OPTION, description = NO_TRUNCATE_OPTION_DESC)
+    private boolean noTruncate;
+
     @Option(names = SCRIPT_FILE_OPTION, description = SCRIPT_FILE_OPTION_DESC)
     private String file;
 
@@ -104,6 +114,12 @@ public class SqlReplCommand extends BaseCommand implements 
Callable<Integer> {
         if (timed) {
             result.add(TIMED_OPTION);
         }
+        if (maxColWidth != null) {
+            result.add(MAX_COL_WIDTH_OPTION + "=" + maxColWidth);
+        }
+        if (noTruncate) {
+            result.add(NO_TRUNCATE_OPTION);
+        }
         if (file != null) {
             result.add(SCRIPT_FILE_OPTION + "=" + file);
         }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
index 82bbe0881d9..c06178c5777 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
@@ -83,7 +83,13 @@ public enum CliConfigKeys {
     PAGER_ENABLED(Constants.PAGER_ENABLED),
 
     /** Pager command property name. */
-    PAGER_COMMAND(Constants.PAGER_COMMAND);
+    PAGER_COMMAND(Constants.PAGER_COMMAND),
+
+    /** Output truncation enabled property name. */
+    OUTPUT_TRUNCATE(Constants.OUTPUT_TRUNCATE),
+
+    /** Maximum column width property name. */
+    OUTPUT_MAX_COLUMN_WIDTH(Constants.OUTPUT_MAX_COLUMN_WIDTH);
 
     private final String value;
 
@@ -157,6 +163,10 @@ public enum CliConfigKeys {
         public static final String PAGER_ENABLED = "ignite.cli.pager.enabled";
 
         public static final String PAGER_COMMAND = "ignite.cli.pager.command";
+
+        public static final String OUTPUT_TRUNCATE = 
"ignite.cli.output.truncate";
+
+        public static final String OUTPUT_MAX_COLUMN_WIDTH = 
"ignite.cli.output.max-column-width";
     }
 
     CliConfigKeys(String value) {
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 2c99e6a2ea8..05b5d7159cb 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
@@ -27,18 +27,31 @@ import org.apache.ignite.internal.cli.sql.SqlQueryResult;
 public class SqlQueryResultDecorator implements Decorator<SqlQueryResult, 
TerminalOutput> {
     private final boolean plain;
     private final boolean timed;
+    private final TruncationConfig truncationConfig;
 
     public SqlQueryResultDecorator(boolean plain) {
-        this(plain, false);
+        this(plain, false, TruncationConfig.disabled());
     }
 
     public SqlQueryResultDecorator(boolean plain, boolean timed) {
+        this(plain, timed, TruncationConfig.disabled());
+    }
+
+    /**
+     * Creates a new SqlQueryResultDecorator with truncation support.
+     *
+     * @param plain whether to use plain formatting
+     * @param timed whether to include execution time
+     * @param truncationConfig truncation configuration
+     */
+    public SqlQueryResultDecorator(boolean plain, boolean timed, 
TruncationConfig truncationConfig) {
         this.plain = plain;
         this.timed = timed;
+        this.truncationConfig = truncationConfig;
     }
 
     @Override
     public TerminalOutput decorate(SqlQueryResult data) {
-        return data.getResult(plain, timed);
+        return data.getResult(plain, timed, truncationConfig);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TableDecorator.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TableDecorator.java
index 14e59d4a644..4351f007c2b 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TableDecorator.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TableDecorator.java
@@ -22,29 +22,54 @@ import 
org.apache.ignite.internal.cli.core.decorator.Decorator;
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
 import org.apache.ignite.internal.cli.sql.table.Table;
 import org.apache.ignite.internal.cli.util.PlainTableRenderer;
+import org.apache.ignite.internal.cli.util.TableTruncator;
 
 /**
  * Implementation of {@link Decorator} for {@link Table}.
+ *
+ * <p>Uses raw {@code Table} type to match the CLI decorator registry 
infrastructure.
+ * The cast to {@code Table<String>} is safe because all Table instances in 
the CLI are String-typed.
  */
 public class TableDecorator implements Decorator<Table, TerminalOutput> {
     private final boolean plain;
+    private final TableTruncator truncator;
 
+    /**
+     * Creates a new TableDecorator with truncation disabled.
+     *
+     * @param plain whether to use plain formatting
+     */
     public TableDecorator(boolean plain) {
+        this(plain, TruncationConfig.disabled());
+    }
+
+    /**
+     * Creates a new TableDecorator with truncation support.
+     *
+     * @param plain whether to use plain formatting
+     * @param truncationConfig truncation configuration
+     */
+    public TableDecorator(boolean plain, TruncationConfig truncationConfig) {
         this.plain = plain;
+        this.truncator = new TableTruncator(truncationConfig);
     }
 
     /**
-     * Transform {@link Table} to {@link TerminalOutput}.
+     * Decorates a table for terminal output.
+     *
+     * <p>Applies truncation based on configuration and renders the table
+     * in either plain or formatted style.
      *
-     * @param table incoming {@link Table}.
-     * @return User-friendly interpretation of {@link Table} in {@link 
TerminalOutput}.
+     * @param table the table to decorate
+     * @return terminal output containing the rendered table
      */
     @Override
     public TerminalOutput decorate(Table table) {
+        Table<String> truncatedTable = truncator.truncate((Table<String>) 
table);
         if (plain) {
-            return () -> PlainTableRenderer.render(table.header(), 
table.content());
+            return () -> PlainTableRenderer.render(truncatedTable.header(), 
truncatedTable.content());
         } else {
-            return () -> FlipTableConverters.fromObjects(table.header(), 
table.content());
+            return () -> 
FlipTableConverters.fromObjects(truncatedTable.header(), 
truncatedTable.content());
         }
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TruncationConfig.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TruncationConfig.java
new file mode 100644
index 00000000000..518a4b32fa9
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/TruncationConfig.java
@@ -0,0 +1,180 @@
+/*
+ * 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.decorators;
+
+import java.util.function.IntSupplier;
+import org.apache.ignite.internal.cli.config.CliConfigKeys;
+import org.apache.ignite.internal.cli.config.ConfigManagerProvider;
+
+/**
+ * Configuration for table column truncation.
+ */
+public class TruncationConfig {
+    /** Default maximum column width. */
+    public static final int DEFAULT_MAX_COLUMN_WIDTH = 50;
+
+    /** Default terminal width used when actual width cannot be detected. */
+    public static final int DEFAULT_TERMINAL_WIDTH = 80;
+
+    /** Truncation indicator (Unicode horizontal ellipsis U+2026). */
+    public static final String ELLIPSIS = "…";
+
+    /** Disabled truncation config instance. */
+    private static final TruncationConfig DISABLED = new 
TruncationConfig(false, DEFAULT_MAX_COLUMN_WIDTH, () -> 0);
+
+    private final boolean truncateEnabled;
+    private final int maxColumnWidth;
+    private final IntSupplier terminalWidthSupplier;
+
+    /**
+     * Creates a new TruncationConfig with a fixed terminal width.
+     *
+     * @param truncateEnabled whether truncation is enabled
+     * @param maxColumnWidth maximum column width
+     * @param terminalWidth terminal width (0 means no terminal width 
constraint)
+     */
+    public TruncationConfig(boolean truncateEnabled, int maxColumnWidth, int 
terminalWidth) {
+        this(truncateEnabled, maxColumnWidth, () -> terminalWidth);
+    }
+
+    /**
+     * Creates a new TruncationConfig with a dynamic terminal width supplier.
+     *
+     * @param truncateEnabled whether truncation is enabled
+     * @param maxColumnWidth maximum column width
+     * @param terminalWidthSupplier supplier for terminal width (evaluated on 
each call)
+     */
+    public TruncationConfig(boolean truncateEnabled, int maxColumnWidth, 
IntSupplier terminalWidthSupplier) {
+        this.truncateEnabled = truncateEnabled;
+        this.maxColumnWidth = maxColumnWidth;
+        this.terminalWidthSupplier = terminalWidthSupplier;
+    }
+
+    /**
+     * Returns a disabled truncation config.
+     *
+     * @return disabled config instance
+     */
+    public static TruncationConfig disabled() {
+        return DISABLED;
+    }
+
+    /**
+     * Creates a TruncationConfig from configuration and command-line 
overrides.
+     *
+     * @param configManagerProvider configuration manager provider
+     * @param terminalWidthSupplier supplier for terminal width (evaluated 
dynamically)
+     * @param maxColWidthOverride command-line override for max column width 
(null to use config)
+     * @param noTruncateFlag command-line flag to disable truncation
+     * @param plainFlag command-line flag for plain output (implies no 
truncation)
+     * @return configured TruncationConfig
+     */
+    public static TruncationConfig fromConfig(
+            ConfigManagerProvider configManagerProvider,
+            IntSupplier terminalWidthSupplier,
+            Integer maxColWidthOverride,
+            boolean noTruncateFlag,
+            boolean plainFlag
+    ) {
+        // Plain output implies no truncation
+        if (noTruncateFlag || plainFlag) {
+            return DISABLED;
+        }
+
+        boolean truncateEnabled = readTruncateEnabled(configManagerProvider);
+        if (!truncateEnabled) {
+            return DISABLED;
+        }
+
+        int maxColumnWidth = maxColWidthOverride != null
+                ? maxColWidthOverride
+                : readMaxColumnWidth(configManagerProvider);
+
+        // Wrap the terminal width supplier with fallback logic
+        IntSupplier widthWithFallback = () -> {
+            int width = terminalWidthSupplier.getAsInt();
+            if (width > 0) {
+                return width;
+            }
+            // Try COLUMNS environment variable as fallback
+            String columnsEnv = System.getenv("COLUMNS");
+            if (columnsEnv != null && !columnsEnv.isEmpty()) {
+                try {
+                    int envWidth = Integer.parseInt(columnsEnv);
+                    if (envWidth > 0) {
+                        return envWidth;
+                    }
+                } catch (NumberFormatException ignored) {
+                    // Fall through to default
+                }
+            }
+            return DEFAULT_TERMINAL_WIDTH;
+        };
+
+        return new TruncationConfig(true, maxColumnWidth, widthWithFallback);
+    }
+
+    /**
+     * Returns whether truncation is enabled.
+     *
+     * @return true if truncation is enabled
+     */
+    public boolean isTruncateEnabled() {
+        return truncateEnabled;
+    }
+
+    /**
+     * Returns the maximum column width.
+     *
+     * @return maximum column width
+     */
+    public int getMaxColumnWidth() {
+        return maxColumnWidth;
+    }
+
+    /**
+     * Returns the terminal width. This value is evaluated dynamically
+     * to support terminal resize during a session.
+     *
+     * @return terminal width (0 means no constraint)
+     */
+    public int getTerminalWidth() {
+        return terminalWidthSupplier.getAsInt();
+    }
+
+    private static boolean readTruncateEnabled(ConfigManagerProvider 
configManagerProvider) {
+        String value = configManagerProvider.get()
+                .getCurrentProperty(CliConfigKeys.OUTPUT_TRUNCATE.value());
+        // Default to true if not set
+        return value == null || value.isEmpty() || Boolean.parseBoolean(value);
+    }
+
+    private static int readMaxColumnWidth(ConfigManagerProvider 
configManagerProvider) {
+        String value = configManagerProvider.get()
+                
.getCurrentProperty(CliConfigKeys.OUTPUT_MAX_COLUMN_WIDTH.value());
+        if (value == null || value.isEmpty()) {
+            return DEFAULT_MAX_COLUMN_WIDTH;
+        }
+        try {
+            int width = Integer.parseInt(value);
+            return width > 0 ? width : DEFAULT_MAX_COLUMN_WIDTH;
+        } catch (NumberFormatException e) {
+            return DEFAULT_MAX_COLUMN_WIDTH;
+        }
+    }
+}
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 1aaa10a8106..fe3097f0fce 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
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
 import org.apache.ignite.internal.cli.sql.table.Table;
 
 /**
@@ -43,15 +44,7 @@ public class SqlQueryResult {
      * @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;
-        };
+        return getResult(plain, timed, TruncationConfig.disabled());
     }
 
     /**
@@ -63,6 +56,26 @@ public class SqlQueryResult {
         return getResult(plain, false);
     }
 
+    /**
+     * SQL query result provider with truncation support.
+     *
+     * @param plain Whether to use plain formatting.
+     * @param timed Whether to include execution time in output.
+     * @param truncationConfig Truncation configuration.
+     * @return terminal output all items in query result.
+     */
+    public TerminalOutput getResult(boolean plain, boolean timed, 
TruncationConfig truncationConfig) {
+        return () -> {
+            String result = sqlQueryResultItems.stream()
+                    .map(x -> x.decorate(plain, 
truncationConfig).toTerminalString())
+                    .collect(Collectors.joining(""));
+            if (timed) {
+                result += "Query executed in " + durationMs + "ms 
(client-side).\n";
+            }
+            return result;
+        };
+    }
+
     /**
      * Builder for {@link SqlQueryResult}.
      */
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultItem.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultItem.java
index 0cebbeb7de7..72d54778687 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultItem.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultItem.java
@@ -18,15 +18,26 @@
 package org.apache.ignite.internal.cli.sql;
 
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
 
 /**
  *  An object that represents a single item of the SQL query result.
  */
 interface SqlQueryResultItem {
     /**
-     * Decorates the item.
+     * Decorates the item with truncation support.
      *
      * @param plain Whether to use plain output.
+     * @param truncationConfig Truncation configuration.
      */
-    TerminalOutput decorate(boolean plain);
+    TerminalOutput decorate(boolean plain, TruncationConfig truncationConfig);
+
+    /**
+     * Decorates the item with default (disabled) truncation.
+     *
+     * @param plain Whether to use plain output.
+     */
+    default TerminalOutput decorate(boolean plain) {
+        return decorate(plain, TruncationConfig.disabled());
+    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultMessage.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultMessage.java
index ec2e0a876e9..8e7d4b35afc 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultMessage.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultMessage.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.sql;
 
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
 import org.apache.ignite.internal.cli.decorators.DefaultDecorator;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
 
 /**
  * A message in the SQL query result.
@@ -32,7 +33,8 @@ class SqlQueryResultMessage implements SqlQueryResultItem {
     }
 
     @Override
-    public TerminalOutput decorate(boolean plain) {
+    public TerminalOutput decorate(boolean plain, TruncationConfig 
truncationConfig) {
+        // Messages are not truncated
         return new DefaultDecorator<String>().decorate(message);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultTable.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultTable.java
index 013054e2cdd..0f05a202443 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultTable.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/sql/SqlQueryResultTable.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.sql;
 
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
 import org.apache.ignite.internal.cli.decorators.TableDecorator;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
 import org.apache.ignite.internal.cli.sql.table.Table;
 
 /**
@@ -33,7 +34,7 @@ class SqlQueryResultTable implements SqlQueryResultItem {
     }
 
     @Override
-    public TerminalOutput decorate(boolean plain) {
-        return new TableDecorator(plain).decorate(table);
+    public TerminalOutput decorate(boolean plain, TruncationConfig 
truncationConfig) {
+        return new TableDecorator(plain, truncationConfig).decorate(table);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/util/TableTruncator.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/util/TableTruncator.java
new file mode 100644
index 00000000000..b119b0ca597
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/util/TableTruncator.java
@@ -0,0 +1,266 @@
+/*
+ * 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.util;
+
+import static 
org.apache.ignite.internal.cli.decorators.TruncationConfig.ELLIPSIS;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
+import org.apache.ignite.internal.cli.sql.table.Table;
+
+/**
+ * Truncates table columns to fit terminal width.
+ */
+public class TableTruncator {
+    /** Minimum column width to display at least one character plus ellipsis. 
*/
+    private static final int MIN_COLUMN_WIDTH = ELLIPSIS.length() + 1;
+
+    /**
+     * Per-column overhead in FlipTables: left padding + right padding + 
separator/border.
+     *
+     * <p>Example for 2 columns (c1, c2):
+     * <pre>
+     * ║ c1 │ c2 ║
+     * </pre>
+     * Each column has: 1 space before + 1 space after + 1 border char = 3 
chars overhead.
+     */
+    private static final int BORDER_OVERHEAD_PER_COLUMN = 3;
+
+    /**
+     * Fixed table border overhead (the extra left border character).
+     *
+     * <p>Total overhead calculation for N columns:
+     * <pre>
+     * ║ c1 │ c2 │ ... │ cN ║
+     * ^    ^    ^          ^
+     * 1    3    3          3  (per-column overhead includes trailing 
separator)
+     * </pre>
+     * The left border (║) is not counted in per-column overhead, so we add 1.
+     * Total overhead = 1 + 3*N.
+     */
+    private static final int TABLE_BORDER_OVERHEAD = 1;
+
+    private final TruncationConfig config;
+
+    /**
+     * Creates a new TableTruncator with the given configuration.
+     *
+     * @param config truncation configuration
+     */
+    public TableTruncator(TruncationConfig config) {
+        this.config = config;
+    }
+
+    /**
+     * Truncates table columns based on the truncation configuration.
+     *
+     * @param table the table to truncate
+     * @return a new table with truncated values, or the original table if 
truncation is disabled
+     */
+    public Table<String> truncate(Table<String> table) {
+        if (!config.isTruncateEnabled()) {
+            return table;
+        }
+
+        String[] header = table.header();
+        Object[][] content = table.content();
+
+        if (header.length == 0) {
+            return table;
+        }
+
+        int[] columnWidths = calculateColumnWidths(header, content);
+        String[] truncatedHeader = truncateRow(header, columnWidths);
+        List<String> truncatedContent = truncateContent(content, columnWidths);
+
+        return new Table<>(Arrays.asList(truncatedHeader), truncatedContent);
+    }
+
+    /**
+     * Calculates optimal column widths based on content and terminal 
constraints.
+     *
+     * @param header table header
+     * @param content table content
+     * @return array of column widths
+     */
+    int[] calculateColumnWidths(String[] header, Object[][] content) {
+        int columnCount = header.length;
+        int[] maxContentWidths = new int[columnCount];
+
+        // Calculate maximum content width for each column
+        for (int col = 0; col < columnCount; col++) {
+            maxContentWidths[col] = Math.max(maxContentWidths[col], 
stringLength(header[col]));
+            for (Object[] row : content) {
+                maxContentWidths[col] = Math.max(maxContentWidths[col], 
stringLength(row[col]));
+            }
+        }
+
+        int[] columnWidths = new int[columnCount];
+        int maxColumnWidth = config.getMaxColumnWidth();
+        int terminalWidth = config.getTerminalWidth();
+
+        // Apply max column width constraint
+        for (int col = 0; col < columnCount; col++) {
+            columnWidths[col] = Math.min(maxContentWidths[col], 
maxColumnWidth);
+            columnWidths[col] = Math.max(columnWidths[col], MIN_COLUMN_WIDTH);
+        }
+
+        // Apply terminal width constraint if set
+        if (terminalWidth > 0) {
+            distributeWidthsForTerminal(columnWidths, terminalWidth);
+        }
+
+        return columnWidths;
+    }
+
+    /**
+     * Distributes column widths to fit within terminal width.
+     *
+     * @param columnWidths column widths to adjust (modified in place)
+     * @param terminalWidth terminal width constraint
+     */
+    private static void distributeWidthsForTerminal(int[] columnWidths, int 
terminalWidth) {
+        int columnCount = columnWidths.length;
+        int totalBorderOverhead = TABLE_BORDER_OVERHEAD + (columnCount * 
BORDER_OVERHEAD_PER_COLUMN);
+        int availableWidth = terminalWidth - totalBorderOverhead;
+
+        if (availableWidth <= 0) {
+            // Terminal too narrow, use minimum widths
+            Arrays.fill(columnWidths, MIN_COLUMN_WIDTH);
+            return;
+        }
+
+        int totalCurrentWidth = Arrays.stream(columnWidths).sum();
+
+        if (totalCurrentWidth <= availableWidth) {
+            // Everything fits, no adjustment needed
+            return;
+        }
+
+        // Need to shrink columns proportionally
+        // First, calculate how much we need to reduce
+        int excessWidth = totalCurrentWidth - availableWidth;
+
+        // Calculate total shrinkable width (columns that are above minimum)
+        int[] shrinkableWidth = new int[columnCount];
+        int totalShrinkable = 0;
+        for (int col = 0; col < columnCount; col++) {
+            shrinkableWidth[col] = columnWidths[col] - MIN_COLUMN_WIDTH;
+            totalShrinkable += shrinkableWidth[col];
+        }
+
+        if (totalShrinkable <= 0) {
+            // All columns at minimum, can't shrink further
+            return;
+        }
+
+        // Shrink proportionally using integer division to avoid over-shrinking
+        int totalReduction = 0;
+        for (int col = 0; col < columnCount; col++) {
+            if (shrinkableWidth[col] > 0) {
+                int reduction = excessWidth * shrinkableWidth[col] / 
totalShrinkable;
+                reduction = Math.min(reduction, shrinkableWidth[col]);
+                columnWidths[col] -= reduction;
+                totalReduction += reduction;
+            }
+        }
+
+        // Distribute any remaining excess (due to integer truncation) one 
column at a time
+        int remaining = excessWidth - totalReduction;
+        for (int col = 0; col < columnCount && remaining > 0; col++) {
+            if (columnWidths[col] > MIN_COLUMN_WIDTH) {
+                columnWidths[col]--;
+                remaining--;
+            }
+        }
+    }
+
+    /**
+     * Truncates a row of values to fit the specified column widths.
+     *
+     * @param row row values
+     * @param columnWidths column widths
+     * @return truncated row values
+     */
+    private static String[] truncateRow(String[] row, int[] columnWidths) {
+        String[] result = new String[row.length];
+        for (int i = 0; i < row.length; i++) {
+            int maxWidth = i < columnWidths.length ? columnWidths[i] : 
Integer.MAX_VALUE;
+            result[i] = truncateCell(row[i], maxWidth);
+        }
+        return result;
+    }
+
+    /**
+     * Truncates all content rows to fit the specified column widths.
+     *
+     * @param content table content
+     * @param columnWidths column widths
+     * @return flat list of truncated values
+     */
+    private static List<String> truncateContent(Object[][] content, int[] 
columnWidths) {
+        List<String> result = new ArrayList<>();
+        for (Object[] row : content) {
+            for (int col = 0; col < row.length; col++) {
+                int maxWidth = col < columnWidths.length ? columnWidths[col] : 
Integer.MAX_VALUE;
+                result.add(truncateCell(row[col], maxWidth));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Truncates a single cell value to fit the specified width.
+     *
+     * @param value cell value
+     * @param maxWidth maximum width
+     * @return truncated value
+     */
+    static String truncateCell(Object value, int maxWidth) {
+        if (value == null) {
+            return "null";
+        }
+
+        String str = String.valueOf(value);
+
+        if (str.length() <= maxWidth) {
+            return str;
+        }
+
+        if (maxWidth <= ELLIPSIS.length()) {
+            return ELLIPSIS.substring(0, maxWidth);
+        }
+
+        return str.substring(0, maxWidth - ELLIPSIS.length()) + ELLIPSIS;
+    }
+
+    /**
+     * Returns the display length of a value.
+     *
+     * @param value value to measure
+     * @return length of string representation
+     */
+    private static int stringLength(Object value) {
+        if (value == null) {
+            return 4; // "null"
+        }
+        return String.valueOf(value).length();
+    }
+}
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/util/TableTruncatorTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/util/TableTruncatorTest.java
new file mode 100644
index 00000000000..9462dd8dc5d
--- /dev/null
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/util/TableTruncatorTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.util;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.sameInstance;
+
+import com.jakewharton.fliptables.FlipTableConverters;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.ignite.internal.cli.decorators.TruncationConfig;
+import org.apache.ignite.internal.cli.sql.table.Table;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link TableTruncator}.
+ */
+class TableTruncatorTest {
+
+    @Test
+    void truncateDisabledReturnsOriginalTable() {
+        Table<String> table = createTable(
+                List.of("id", "name"),
+                List.of("1", "Alice", "2", "Bob")
+        );
+        TruncationConfig config = TruncationConfig.disabled();
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result, sameInstance(table));
+    }
+
+    @Test
+    void truncateCellWithinLimit() {
+        String result = TableTruncator.truncateCell("short", 10);
+
+        assertThat(result, equalTo("short"));
+    }
+
+    @Test
+    void truncateCellExceedsLimit() {
+        String result = TableTruncator.truncateCell("very long text that 
exceeds the limit", 10);
+
+        assertThat(result, equalTo("very long…"));
+    }
+
+    @Test
+    void truncateCellExactlyAtLimit() {
+        String result = TableTruncator.truncateCell("1234567890", 10);
+
+        assertThat(result, equalTo("1234567890"));
+    }
+
+    @Test
+    void truncateCellNullValue() {
+        String result = TableTruncator.truncateCell(null, 10);
+
+        assertThat(result, equalTo("null"));
+    }
+
+    @Test
+    void truncateCellMinimumWidth() {
+        // With 1-char ellipsis, width 2 = 1 char content + ellipsis
+        String result = TableTruncator.truncateCell("hello", 2);
+
+        assertThat(result, equalTo("h…"));
+    }
+
+    @Test
+    void truncateCellWidthEqualToEllipsis() {
+        // With 1-char ellipsis, width 1 = just the ellipsis
+        String result = TableTruncator.truncateCell("hello", 1);
+
+        assertThat(result, equalTo("…"));
+    }
+
+    @Test
+    void truncateTableAppliesMaxColumnWidth() {
+        Table<String> table = createTable(
+                List.of("id", "description"),
+                List.of("1", "This is a very long description that should be 
truncated")
+        );
+        TruncationConfig config = new TruncationConfig(true, 20, 0);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result.header()[1], equalTo("description"));
+        assertThat(result.content()[0][1], equalTo("This is a very long…"));
+    }
+
+    @Test
+    void truncateTablePreservesShortColumns() {
+        Table<String> table = createTable(
+                List.of("id", "name", "description"),
+                List.of("1", "Alice", "Short desc")
+        );
+        TruncationConfig config = new TruncationConfig(true, 20, 0);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result.content()[0][0], equalTo("1"));
+        assertThat(result.content()[0][1], equalTo("Alice"));
+        assertThat(result.content()[0][2], equalTo("Short desc"));
+    }
+
+    @Test
+    void truncateEmptyTableReturnsOriginal() {
+        Table<String> table = createTable(List.of("id"), List.of());
+        TruncationConfig config = new TruncationConfig(true, 20, 80);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result.header().length, is(1));
+        assertThat(result.content().length, is(0));
+    }
+
+    @Test
+    void calculateColumnWidthsRespectsMaxColumnWidth() {
+        String[] header = {"id", "description"};
+        Object[][] content = {{"1", "This is a long description"}};
+        TruncationConfig config = new TruncationConfig(true, 15, 0);
+
+        int[] widths = new 
TableTruncator(config).calculateColumnWidths(header, content);
+
+        assertThat(widths[0], is(2)); // minimum column width (1 char + 1-char 
ellipsis)
+        assertThat(widths[1], is(15)); // capped at max column width
+    }
+
+    @Test
+    void calculateColumnWidthsDistributesForTerminal() {
+        String[] header = {"col1", "col2", "col3"};
+        Object[][] content = {
+                {"very long content 1", "very long content 2", "very long 
content 3"}
+        };
+        // Terminal width = 50, with overhead for borders (3*3 + 1 = 10)
+        TruncationConfig config = new TruncationConfig(true, 100, 50);
+
+        int[] widths = new 
TableTruncator(config).calculateColumnWidths(header, content);
+
+        // Total width should fit within terminal width (available = 50 - 10 = 
40)
+        int totalWidth = Arrays.stream(widths).sum();
+        assertThat(totalWidth, lessThanOrEqualTo(50 - 10));
+
+        // Each column should have reasonable width (at least minimum of 2: 1 
char + 1-char ellipsis)
+        for (int width : widths) {
+            assertThat(width, greaterThanOrEqualTo(2));
+        }
+
+        // Verify actual truncation result
+        Table<String> table = createTable(List.of("col1", "col2", "col3"),
+                List.of("very long content 1", "very long content 2", "very 
long content 3"));
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        // Content should be truncated to fit calculated widths
+        for (int i = 0; i < 3; i++) {
+            String cell = (String) result.content()[0][i];
+            assertThat(cell.length(), lessThanOrEqualTo(widths[i]));
+        }
+    }
+
+    @Test
+    void calculateColumnWidthsUsesExactAvailableWidth() {
+        // Single column with content larger than terminal
+        String[] header = {"header"};
+        Object[][] content = {{"12345678901234567890"}}; // 20 chars
+
+        // Terminal = 20, overhead for 1 column = 3*1 + 1 = 4, available = 16
+        TruncationConfig config = new TruncationConfig(true, 100, 20);
+
+        int[] widths = new 
TableTruncator(config).calculateColumnWidths(header, content);
+
+        // Content (20) exceeds available (16), so should be shrunk to exactly 
16
+        assertThat(widths[0], is(16));
+    }
+
+    @Test
+    void calculateColumnWidthsExactFitWhenResizedByOne() {
+        // Simulate: content fits at width W, then terminal resized to W-1
+        String[] header = {"col"};
+        Object[][] content = {{"1234567890"}}; // 10 chars
+
+        // At terminal = 14: overhead = 3*1 + 1 = 4, available = 10, content = 
10 -> fits exactly
+        TruncationConfig configFits = new TruncationConfig(true, 100, 14);
+        int[] widthsFits = new 
TableTruncator(configFits).calculateColumnWidths(header, content);
+        assertThat(widthsFits[0], is(10)); // No truncation needed
+
+        // At terminal = 13: overhead = 4, available = 9, content = 10 -> need 
to shrink by 1
+        TruncationConfig configShrink = new TruncationConfig(true, 100, 13);
+        int[] widthsShrink = new 
TableTruncator(configShrink).calculateColumnWidths(header, content);
+        assertThat(widthsShrink[0], is(9)); // Should shrink to exactly 9, not 
less
+    }
+
+    @Test
+    void truncateTableWithMultipleRows() {
+        Table<String> table = createTable(
+                List.of("id", "name"),
+                List.of(
+                        "1", "Alice with a very long name",
+                        "2", "Bob",
+                        "3", "Charlie with another long name"
+                )
+        );
+        TruncationConfig config = new TruncationConfig(true, 10, 0);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result.content()[0][1], equalTo("Alice wit…"));
+        assertThat(result.content()[1][1], equalTo("Bob"));
+        assertThat(result.content()[2][1], equalTo("Charlie w…"));
+    }
+
+    @Test
+    void truncatePreservesHeaderWhenWidthIsSmall() {
+        Table<String> table = createTable(
+                List.of("very_long_header_name"),
+                List.of("short")
+        );
+        TruncationConfig config = new TruncationConfig(true, 10, 0);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        assertThat(result.header()[0], equalTo("very_long…"));
+    }
+
+    @Test
+    void truncateTableWithLongContentExceedingDefaultMaxWidth() {
+        // Content longer than default max column width (50 characters)
+        String longContent = "This is a very long string that exceeds the 
default maximum column width of fifty characters";
+        Table<String> table = createTable(
+                List.of("id", "description"),
+                List.of("1", longContent)
+        );
+        TruncationConfig config = new TruncationConfig(true, 50, 0);
+
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        String truncatedContent = (String) result.content()[0][1];
+        assertThat(truncatedContent.length(), is(50));
+        assertThat(truncatedContent, equalTo("This is a very long string that 
exceeds the defau…"));
+    }
+
+    /**
+     * Tests that a truncated table with IDDDDD column fits exactly within the 
terminal width.
+     * This is a regression test for the off-by-1 bug in the overhead 
calculation.
+     *
+     * <p>The FlipTables overhead formula is: 3*N + 1 where N is the number of 
columns.
+     * - Left border: 1 char
+     * - Right border: 1 char
+     * - N-1 separators between columns: N-1 chars
+     * - 2*N padding (space before + space after each column): 2*N chars
+     * - Total: 1 + 1 + (N-1) + 2*N = 3*N + 1
+     */
+    @Test
+    void truncatedTableWithIdddddColumnFitsWithinTerminalWidth() {
+        // Simulate the user's table: IDDDDD, NAME, AGE, EMAIL
+        Table<String> table = createTable(
+                List.of("IDDDDD", "NAME", "AGE", "EMAIL"),
+                List.of("1", "Alice", "28", "[email protected]")
+        );
+
+        // Set terminal width to a value that requires truncation
+        int terminalWidth = 50;
+        TruncationConfig config = new TruncationConfig(true, 100, 
terminalWidth);
+
+        Table<String> truncated = new TableTruncator(config).truncate(table);
+
+        // Render the table using FlipTables (same as TableDecorator does)
+        String rendered = FlipTableConverters.fromObjects(truncated.header(), 
truncated.content());
+
+        // Get the actual width of the rendered table (first line contains the 
top border)
+        int actualWidth = rendered.lines().findFirst().orElse("").length();
+
+        assertThat("Rendered table width should fit within terminal width",
+                actualWidth, lessThanOrEqualTo(terminalWidth));
+    }
+
+    /**
+     * Tests that a truncated table fits exactly within the terminal width when
+     * the content needs to be shrunk.
+     * This is a regression test for the off-by-1 bug in the overhead 
calculation.
+     *
+     * <p>For 4 columns:
+     * - FlipTables overhead = 3*N + 1 = 3*4 + 1 = 13
+     * - If we calculate overhead as 3*N = 12 (wrong), we'll allow 1 extra char
+     *   of content, causing the table to overflow by 1 character.
+     */
+    @Test
+    void truncatedTableFitsExactlyWhenContentNeedsShrinking() {
+        // Create a table with long content that requires truncation
+        // IDDDDD=6, NAME=7 (Charlie), AGE=3, EMAIL=19 ([email protected])
+        // Total content = 35, Overhead = 13, Natural width = 48
+        Table<String> table = createTable(
+                List.of("IDDDDD", "NAME", "AGE", "EMAIL"),
+                List.of("1", "Charlie", "42", "[email protected]")
+        );
+
+        // Set terminal width smaller than natural width to force truncation
+        // Natural width = 48, so terminal = 40 forces shrinking
+        int terminalWidth = 40;
+        TruncationConfig config = new TruncationConfig(true, 100, 
terminalWidth);
+
+        Table<String> truncated = new TableTruncator(config).truncate(table);
+
+        // Render the table using FlipTables
+        String rendered = FlipTableConverters.fromObjects(truncated.header(), 
truncated.content());
+        int actualWidth = rendered.lines().findFirst().orElse("").length();
+
+        // The bug: if overhead is calculated as 12 instead of 13,
+        // the table will be 1 char wider than terminal
+        assertThat("Rendered table width should not exceed terminal width. "
+                        + "Actual width: " + actualWidth + ", terminal: " + 
terminalWidth,
+                actualWidth, lessThanOrEqualTo(terminalWidth));
+    }
+
+    /**
+     * Tests that when terminal width is 0, truncation still works using the 
default width fallback.
+     * This is important for environments where JLine cannot detect terminal 
size.
+     */
+    @Test
+    void truncateTableWorksWhenTerminalWidthIsZero() {
+        // Create a table with content that would overflow a narrow terminal
+        Table<String> table = createTable(
+                List.of("A_VERY_LONG_COLUMN_HEADER", "ANOTHER_LONG_HEADER"),
+                List.of("some long content that exceeds normal width", "more 
long content here")
+        );
+
+        // Terminal width 0 means "not detected" - should use fallback
+        TruncationConfig config = new TruncationConfig(true, 100, 0);
+
+        // When terminal width is 0, NO terminal-based truncation is applied
+        // Only maxColumnWidth truncation is applied
+        Table<String> result = new TableTruncator(config).truncate(table);
+
+        // The table should still be truncated based on maxColumnWidth (100)
+        // But since content is shorter than 100, it should remain unchanged
+        assertThat(result.content()[0][0], equalTo("some long content that 
exceeds normal width"));
+    }
+
+    /**
+     * Tests that TruncationConfig.fromConfig applies fallback when terminal 
returns 0.
+     */
+    @Test
+    void fromConfigAppliesFallbackWhenTerminalWidthIsZero() {
+        // This test verifies the fallback logic in TruncationConfig.fromConfig
+        // by checking that it uses DEFAULT_TERMINAL_WIDTH when supplier 
returns 0
+        assertThat(TruncationConfig.DEFAULT_TERMINAL_WIDTH, is(80));
+    }
+
+    private static Table<String> createTable(List<String> headers, 
List<String> content) {
+        return new Table<>(headers, content);
+    }
+
+    private static TruncationConfig enabledConfig(int maxColumnWidth) {
+        return new TruncationConfig(true, maxColumnWidth, 0);
+    }
+}

Reply via email to