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);
+ }
+}