summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCopilot <[email protected]>2026-01-14 11:52:07 +0300
committerGitHub <[email protected]>2026-01-14 11:52:07 +0300
commit2b1de06f31ceb240facce913ec47d504032c409a (patch)
tree31cef97c76df9967ef78c5be43b5dc5accc6cf45
parent1b5cf02b9804fb36dec35cf90b3ac29c82bd04c9 (diff)
Unify time interval format across YDB CLI commands (#31593)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: asmyasnikov <[email protected]> Co-authored-by: pnv1 <[email protected]> Co-authored-by: Aleksey Myasnikov <[email protected]> Co-authored-by: Copilot <[email protected]>
-rw-r--r--ydb/apps/ydb/CHANGELOG.md1
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp9
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_command.cpp8
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_command.h9
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_common.h10
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_service_import.cpp6
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_service_table.cpp7
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_service_topic.cpp24
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_workload.cpp23
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_workload.h6
-rw-r--r--ydb/public/lib/ydb_cli/common/describe.cpp40
-rw-r--r--ydb/public/lib/ydb_cli/common/duration.cpp70
-rw-r--r--ydb/public/lib/ydb_cli/common/duration.h41
-rw-r--r--ydb/public/lib/ydb_cli/common/duration_ut.cpp135
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/ya.make1
-rw-r--r--ydb/public/lib/ydb_cli/common/ya.make1
16 files changed, 296 insertions, 95 deletions
diff --git a/ydb/apps/ydb/CHANGELOG.md b/ydb/apps/ydb/CHANGELOG.md
index ca2b2a814e5..d6dc8d185ea 100644
--- a/ydb/apps/ydb/CHANGELOG.md
+++ b/ydb/apps/ydb/CHANGELOG.md
@@ -1,6 +1,7 @@
* Added the `Created by`, `Create time` and `End time` fields to the "build index" and the "execute script" operations in the `ydb operation` subcommands.
* Added the `--include-index-data` option to the `ydb export s3` command, enabling index data export.
* Added the `--index-population-mode` option to the `ydb import s3` command, allowing selection of the index population mode (e.g. build or import).
+* Added unified time interval format support across CLI commands. Options accepting time durations now support explicit time units (e.g., `5s`, `2m`, `1h`) while maintaining backward compatibility with plain numbers interpreted using their original default units.
## 2.28.0 ##
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp b/ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp
index f7090dec0de..2648decd1de 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp
@@ -3,6 +3,7 @@
#include <ydb/public/lib/ydb_cli/common/format.h>
#include <ydb/public/lib/ydb_cli/common/plan2svg.h>
#include <ydb/public/lib/ydb_cli/common/pretty_table.h>
+#include <ydb/public/lib/ydb_cli/common/duration.h>
#include <library/cpp/json/json_writer.h>
#include <util/stream/null.h>
#include <util/string/printf.h>
@@ -90,11 +91,11 @@ void TWorkloadCommandBenchmark::Config(TConfig& config) {
config.Opts->AddLongOption('v', "verbose", "Verbose output").NoArgument().StoreValue(&VerboseLevel, 1);
- config.Opts->AddLongOption("global-timeout", "Global timeout for all requests")
- .StoreResult(&GlobalTimeout);
+ config.Opts->AddLongOption("global-timeout", "Global timeout for all requests. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .StoreMappedResult(&GlobalTimeout, &ParseDurationMilliseconds);
- config.Opts->AddLongOption("request-timeout", "Timeout for each iteration of each request")
- .StoreResult(&RequestTimeout);
+ config.Opts->AddLongOption("request-timeout", "Timeout for each iteration of each request. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .StoreMappedResult(&RequestTimeout, &ParseDurationMilliseconds);
config.Opts->AddLongOption('t', "threads", "Number of parallel threads in workload")
.StoreResult(&Threads).DefaultValue(Threads).RequiredArgument("COUNT");
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_command.cpp b/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
index b8972e3ef87..b7494a79d2d 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
@@ -41,8 +41,8 @@ void TYdbSimpleCommand::Config(TConfig& config) {
TClientCommand::Config(config);
TClientCommandOptions& opts = *config.Opts;
- opts.AddLongOption("timeout", "Client timeout. There is no point waiting for the result after this long.")
- .RequiredArgument("ms").StoreResult(&ClientTimeout);
+ opts.AddLongOption("timeout", "Client timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .RequiredArgument("DURATION").StoreResult(&ClientTimeout);
}
TYdbOperationCommand::TYdbOperationCommand(const TString& name, const std::initializer_list<TString>& aliases, const TString& description)
@@ -53,9 +53,9 @@ void TYdbOperationCommand::Config(TConfig& config) {
TYdbCommand::Config(config);
TClientCommandOptions& opts = *config.Opts;
- opts.AddLongOption("timeout", "Operation timeout. Operation should be executed on server within this timeout. "
+ opts.AddLongOption("timeout", "Operation timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds. "
"There could also be a delay up to 200ms to receive timeout error from server.")
- .RequiredArgument("ms").StoreResult(&OperationTimeout);
+ .RequiredArgument("DURATION").StoreResult(&OperationTimeout);
}
} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_command.h b/ydb/public/lib/ydb_cli/commands/ydb_command.h
index 60e62b906d7..1157b77bfc0 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_command.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_command.h
@@ -1,6 +1,7 @@
#pragma once
#include <ydb/public/lib/ydb_cli/common/command.h>
+#include <ydb/public/lib/ydb_cli/commands/ydb_common.h>
#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h>
#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/draft/ydb_scripting.h>
@@ -49,7 +50,7 @@ protected:
template<typename TSettingsType>
TSettingsType&& FillSettings(TSettingsType&& settings) {
if (ClientTimeout) {
- settings.ClientTimeout(TDuration::MilliSeconds(FromString<ui64>(ClientTimeout)));
+ settings.ClientTimeout(ParseDurationMilliseconds(ClientTimeout));
}
return std::forward<TSettingsType>(settings);
}
@@ -70,9 +71,9 @@ protected:
template<typename TSettingsType>
TSettingsType&& FillSettings(TSettingsType&& settings) {
if (OperationTimeout) {
- ui64 operationTimeout = FromString<ui64>(OperationTimeout);
- settings.OperationTimeout(TDuration::MilliSeconds(operationTimeout));
- settings.ClientTimeout(TDuration::MilliSeconds(operationTimeout + 200));
+ TDuration timeout = ParseDurationMilliseconds(OperationTimeout);
+ settings.OperationTimeout(timeout);
+ settings.ClientTimeout(timeout + TDuration::MilliSeconds(200));
}
return std::forward<TSettingsType>(settings);
}
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_common.h b/ydb/public/lib/ydb_cli/commands/ydb_common.h
index 2af6a3d8a32..ec6c6377c1c 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_common.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_common.h
@@ -3,7 +3,7 @@
#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h>
#include <ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/types/operation/operation.h>
#include <ydb/public/lib/ydb_cli/common/command.h>
-#include <util/string/strip.h>
+#include <ydb/public/lib/ydb_cli/common/duration.h>
namespace NYdb::NConsoleClient {
@@ -14,12 +14,4 @@ inline void ThrowOnError(const NYdb::TOperation& operation) {
NStatusHelpers::ThrowOnError(operation.Status());
}
-inline TDuration ParseDuration(TStringBuf str) {
- StripInPlace(str);
- if (!str.empty() && !IsAsciiAlpha(str.back())) {
- throw TMisuseException() << "Duration must end with a unit name (ex. 'h' for hours, 's' for seconds)";
- }
- return TDuration::Parse(str);
-}
-
} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_service_import.cpp b/ydb/public/lib/ydb_cli/commands/ydb_service_import.cpp
index 9060f30dfcb..aed83dfca66 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_service_import.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_service_import.cpp
@@ -428,9 +428,9 @@ void TCommandImportFileBase::Config(TConfig& config) {
config.Opts->GetOpts().SetTrailingArgTitle("<input files...>",
"One or more file paths to import from. If a directory is provided, all files in the directory with supported extension will be imported.");
- config.Opts->AddLongOption("timeout", "Operation timeout. Operation should be executed on server within this timeout. "
- "There could also be a delay up to 200ms to receive timeout error from server")
- .RequiredArgument("VAL").StoreResult(&OperationTimeout).DefaultValue(TDuration::Seconds(5 * 60));
+ config.Opts->AddLongOption("timeout", "Operation timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds. "
+ "There could also be a delay up to 200ms to receive timeout error from server.")
+ .RequiredArgument("DURATION").StoreMappedResult(&OperationTimeout, &ParseDurationMilliseconds).DefaultValue(TDuration::Seconds(5 * 60));
config.Opts->AddLongOption('p', "path", "Database path to table")
.Required().RequiredArgument("STRING").StoreResult(&Path);
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_service_table.cpp b/ydb/public/lib/ydb_cli/commands/ydb_service_table.cpp
index 6c987df8f07..5c18b458de0 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_service_table.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_service_table.cpp
@@ -1,4 +1,5 @@
#include "ydb_service_table.h"
+#include "ydb_common.h"
#include <ydb/public/lib/json_value/ydb_json_value.h>
#include <ydb/public/lib/ydb_cli/common/colors.h>
@@ -1492,10 +1493,8 @@ void TCommandTtlSet::Config(TConfig& config) {
ColumnUnit = value;
});
- config.Opts->AddLongOption("run-interval", "[Advanced] How often to run cleanup operation on the same partition.")
- .RequiredArgument("SECONDS").GetOpt().Handler1T<TDuration::TValue>([this](const TDuration::TValue& arg) {
- RunInterval = TDuration::Seconds(arg);
- });
+ config.Opts->AddLongOption("run-interval", "[Advanced] How often to run cleanup operation on the same partition. Supports time units (e.g., '5s', '1m'). Plain number interpreted as seconds.")
+ .RequiredArgument("DURATION").StoreMappedResult(&RunInterval, &ParseDurationSeconds);
config.SetFreeArgsNum(1);
SetFreeArgTitle(0, "<table path>", "Path to a table");
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_service_topic.cpp b/ydb/public/lib/ydb_cli/commands/ydb_service_topic.cpp
index d6fd757a170..851c66849a2 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_service_topic.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_service_topic.cpp
@@ -167,20 +167,6 @@ namespace NYdb::NConsoleClient {
return exists->second;
}
-
- TDuration ParseDurationHours(const TStringBuf str) {
- double hours = 0;
- if (!TryFromString(str, hours)) {
- throw TMisuseException() << "Invalid hours duration '" << str << "'";
- }
- if (hours < 0) {
- throw TMisuseException() << "Duration must be non-negative";
- }
- if (!std::isfinite(hours)) {
- throw TMisuseException() << "Duration must be finite";
- }
- return TDuration::Seconds(hours * 3600); // using floating-point ctor with saturation
- }
}
void TCommandWithSupportedCodecs::AddAllowedCodecs(TClientCommand::TConfig& config, const TVector<NYdb::NTopic::ECodec>& supportedCodecs) {
@@ -363,7 +349,7 @@ namespace NYdb::NConsoleClient {
.StoreResult(&MinActivePartitions_)
.DefaultValue(1);
config.Opts->AddLongOption("retention-period-hours", TStringBuilder()
- << "Duration in hours for which data in topic is stored "
+ << "Duration in hours for which data in topic is stored. Supports time units (e.g., '72h', '1440m'). Plain number interpreted as hours "
<< "(default: " << NColorizer::StdOut().CyanColor() << RetentionPeriod_.Hours() << NColorizer::StdOut().OldColor() << ")")
.Hidden()
.Optional()
@@ -463,7 +449,7 @@ namespace NYdb::NConsoleClient {
config.Opts->AddLongOption("partitions-count", "Initial and minimum number of partitions for topic")
.Optional()
.StoreResult(&MinActivePartitions_);
- config.Opts->AddLongOption("retention-period-hours", "Duration in hours for which data in topic is stored")
+ config.Opts->AddLongOption("retention-period-hours", "Duration in hours for which data in topic is stored. Supports time units (e.g., '72h', '1440m'). Plain number interpreted as hours.")
.Hidden()
.Optional()
.RequiredArgument("HOURS")
@@ -948,7 +934,7 @@ namespace NYdb::NConsoleClient {
config.Opts->AddLongOption('f', "file", "File to write data to. In not specified, data is written to the standard output.")
.Optional()
.StoreResult(&File_);
- config.Opts->AddLongOption("idle-timeout", "Max wait duration for the first message. Topic is considered empty if no new messages arrive within this period.")
+ config.Opts->AddLongOption("idle-timeout", "Max wait duration for the first message. Topic is considered empty if no new messages arrive within this period. Value must include a time unit suffix (e.g., '5s', '1m').")
.Optional()
.DefaultValue(DefaultIdleTimeout)
.StoreResult(&IdleTimeout_);
@@ -1176,10 +1162,10 @@ namespace NYdb::NConsoleClient {
.Optional()
.RequiredArgument("INDEX")
.StoreResult(&PartitionId_);
- config.Opts->AddLongOption("init-seqno-timeout", "Max wait duration for initial seqno")
+ config.Opts->AddLongOption("init-seqno-timeout", "Max wait duration for initial seqno. Supports time units (e.g., '5s', '1m'). Plain number interpreted as seconds.")
.Optional()
.Hidden()
- .Handler([this](const TString& arg) { MessagesWaitTimeout_ = TDuration::Seconds(FromString<ui8>(arg)); });
+ .StoreMappedResult(&MessagesWaitTimeout_, &ParseDurationSeconds);
AddTransform(config);
}
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_workload.cpp b/ydb/public/lib/ydb_cli/commands/ydb_workload.cpp
index 3afc524eb36..70b551849a4 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_workload.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_workload.cpp
@@ -84,9 +84,6 @@ TWorkloadCommand::TWorkloadCommand(const TString& name, const std::initializer_l
, TotalSec(0)
, Threads(0)
, Rate(0)
- , ClientTimeoutMs(0)
- , OperationTimeoutMs(0)
- , CancelAfterTimeoutMs(0)
, WindowSec(0)
, Quiet(false)
, Verbose(false)
@@ -123,12 +120,12 @@ void TWorkloadCommand::Config(TConfig& config) {
.StoreTrue(&Quiet);
config.Opts->AddLongOption("print-timestamp", "Print timestamp each second with statistics.")
.StoreTrue(&PrintTimestamp);
- config.Opts->AddLongOption("client-timeout", "Client timeout in ms.")
- .DefaultValue(1000).StoreResult(&ClientTimeoutMs);
- config.Opts->AddLongOption("operation-timeout", "Operation timeout in ms.")
- .DefaultValue(800).StoreResult(&OperationTimeoutMs);
- config.Opts->AddLongOption("cancel-after", "Cancel after timeout in ms.")
- .DefaultValue(800).StoreResult(&CancelAfterTimeoutMs);
+ config.Opts->AddLongOption("client-timeout", "Client timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .DefaultValue("1000").StoreResult(&ClientTimeoutStr);
+ config.Opts->AddLongOption("operation-timeout", "Operation timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .DefaultValue("800").StoreResult(&OperationTimeoutStr);
+ config.Opts->AddLongOption("cancel-after", "Cancel after timeout. Supports time units (e.g., '5s', '1m'). Plain number interpreted as milliseconds.")
+ .DefaultValue("800").StoreResult(&CancelAfterTimeoutStr);
config.Opts->AddLongOption("window", "Window duration in seconds.")
.DefaultValue(1).StoreResult(&WindowSec);
config.Opts->AddLongOption("executer", "Query executer type (data or generic).")
@@ -170,11 +167,11 @@ void TWorkloadCommand::PrepareForRun(TConfig& config) {
void TWorkloadCommand::WorkerFn(int taskId, NYdbWorkload::IWorkloadQueryGenerator& workloadGen, const int type) {
const auto dataQuerySettings = NYdb::NTable::TExecDataQuerySettings()
.KeepInQueryCache(true)
- .OperationTimeout(TDuration::MilliSeconds(OperationTimeoutMs))
- .ClientTimeout(TDuration::MilliSeconds(ClientTimeoutMs))
- .CancelAfter(TDuration::MilliSeconds(CancelAfterTimeoutMs));
+ .OperationTimeout(ParseDurationMilliseconds(OperationTimeoutStr))
+ .ClientTimeout(ParseDurationMilliseconds(ClientTimeoutStr))
+ .CancelAfter(ParseDurationMilliseconds(CancelAfterTimeoutStr));
const auto genericQuerySettings = NYdb::NQuery::TExecuteQuerySettings()
- .ClientTimeout(TDuration::MilliSeconds(ClientTimeoutMs));
+ .ClientTimeout(ParseDurationMilliseconds(ClientTimeoutStr));
int retryCount = -1;
NYdbWorkload::TQueryInfo queryInfo;
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_workload.h b/ydb/public/lib/ydb_cli/commands/ydb_workload.h
index 271fce87676..cd51f7c4e33 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_workload.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_workload.h
@@ -44,9 +44,9 @@ protected:
size_t TotalSec;
size_t Threads;
ui64 Rate;
- unsigned int ClientTimeoutMs;
- unsigned int OperationTimeoutMs;
- unsigned int CancelAfterTimeoutMs;
+ TString ClientTimeoutStr;
+ TString OperationTimeoutStr;
+ TString CancelAfterTimeoutStr;
unsigned int WindowSec;
bool Quiet;
bool Verbose;
diff --git a/ydb/public/lib/ydb_cli/common/describe.cpp b/ydb/public/lib/ydb_cli/common/describe.cpp
index bbe06bc6282..8ce8ffcf866 100644
--- a/ydb/public/lib/ydb_cli/common/describe.cpp
+++ b/ydb/public/lib/ydb_cli/common/describe.cpp
@@ -21,37 +21,13 @@ namespace NYdb::NConsoleClient {
namespace {
-struct TPrettyDurationFormatParameters {
- double EntitiesPerSecond;
- TStringBuf EntityName;
- TStringBuf ZeroString;
- TStringBuf MaxString;
- int MaxPrecision;
-};
-
-constexpr TPrettyDurationFormatParameters PRETTY_HOURS_DEFAULT{
- .EntitiesPerSecond = 3600,
- .EntityName = "hours",
- .ZeroString = "0 hours",
- .MaxString = "+infinity",
- .MaxPrecision = 3,
-};
-
-constexpr TPrettyDurationFormatParameters PRETTY_HOURS_NON_ZERO{
- .EntitiesPerSecond = 3600,
- .EntityName = "hours",
- .ZeroString = "",
- .MaxString = "+infinity",
- .MaxPrecision = 3,
-};
-
-TString PrettyDurationString(TDuration duration, const TPrettyDurationFormatParameters& paramerters) {
+// Helper function to format duration in human-readable format
+// Returns empty string for zero duration, otherwise returns duration in format like "2h", "5m 30s", etc.
+TString HumanReadableDurationOrEmpty(TDuration duration) {
if (duration == TDuration::Zero()) {
- return ToString(paramerters.ZeroString);
- } else if (duration == TDuration::Max()) {
- return ToString(paramerters.MaxString);
+ return "";
}
- return TStringBuilder() << Prec(duration.MillisecondsFloat() / (paramerters.EntitiesPerSecond * 1000.0), PREC_POINT_DIGITS_STRIP_ZEROES, paramerters.MaxPrecision) << " " << paramerters.EntityName;
+ return ToString(HumanReadable(duration));
}
TString FormatCodecs(const std::vector<NYdb::NTopic::ECodec>& codecs) {
@@ -69,7 +45,7 @@ void PrintTopicConsumers(const std::vector<NYdb::NTopic::TConsumer>& consumers,
.Column(1, FormatCodecs(c.GetSupportedCodecs()))
.Column(2, c.GetReadFrom().ToRfc822StringLocal())
.Column(3, c.GetImportant() ? "Yes" : "No")
- .Column(4, PrettyDurationString(c.GetAvailabilityPeriod(), PRETTY_HOURS_NON_ZERO));
+ .Column(4, HumanReadableDurationOrEmpty(c.GetAvailabilityPeriod()));
}
out << Endl << "Consumers: " << Endl;
out << table;
@@ -88,7 +64,7 @@ void PrintStatistics(const NTopic::TTopicDescription& topicDescription, IOutputS
void PrintMain(const NTopic::TTopicDescription& topicDescription, IOutputStream& out) {
out << Endl << "Main:";
- out << Endl << "RetentionPeriod: " << PrettyDurationString(topicDescription.GetRetentionPeriod(), PRETTY_HOURS_DEFAULT);
+ out << Endl << "RetentionPeriod: " << HumanReadable(topicDescription.GetRetentionPeriod());
if (topicDescription.GetRetentionStorageMb().has_value()) {
out << Endl << "StorageRetention: " << *topicDescription.GetRetentionStorageMb() << " MB";
}
@@ -171,7 +147,7 @@ int PrintPrettyDescribeConsumerResult(const NYdb::NTopic::TConsumerDescription&
const NYdb::NTopic::TConsumer& consumer = description.GetConsumer();
out << "Consumer " << consumer.GetConsumerName() << ": " << Endl;
out << "Important: " << (consumer.GetImportant() ? "Yes" : "No") << Endl;
- if (const auto availabilityPeriodStr = PrettyDurationString(consumer.GetAvailabilityPeriod(), PRETTY_HOURS_NON_ZERO)) {
+ if (const auto availabilityPeriodStr = HumanReadableDurationOrEmpty(consumer.GetAvailabilityPeriod())) {
out << "Availability period: " << availabilityPeriodStr << Endl;
}
if (const TInstant& readFrom = consumer.GetReadFrom()) {
diff --git a/ydb/public/lib/ydb_cli/common/duration.cpp b/ydb/public/lib/ydb_cli/common/duration.cpp
new file mode 100644
index 00000000000..01e85be02d3
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/duration.cpp
@@ -0,0 +1,70 @@
+#include "duration.h"
+
+namespace NYdb::NConsoleClient {
+
+TDuration ParseDuration(TStringBuf str) {
+ StripInPlace(str);
+ if (str.empty()) {
+ throw TMisuseException() << "Duration cannot be empty";
+ }
+ if (!IsAsciiAlpha(str.back())) {
+ throw TMisuseException() << "Duration must end with a unit name (ex. 'h' for hours, 's' for seconds)";
+ }
+ return TDuration::Parse(str);
+}
+
+TDuration ParseDurationWithDefaultUnit(TStringBuf str, TDuration (*defaultUnit)(double)) {
+ StripInPlace(str);
+ if (str.empty()) {
+ throw TMisuseException() << "Duration cannot be empty";
+ }
+
+ // Check if the string ends with a unit (alphabetic character)
+ if (IsAsciiAlpha(str.back())) {
+ // New format with explicit unit (e.g., "5s", "2m", "1h")
+ // TDuration::Parse already supports fractional values
+ return TDuration::Parse(str);
+ }
+
+ // Old format: plain number - interpret using default unit
+ double value = 0;
+ if (!TryFromString(str, value)) {
+ throw TMisuseException() << "Invalid duration value '" << str << "'";
+ }
+ if (value < 0) {
+ throw TMisuseException() << "Duration must be non-negative";
+ }
+ if (!std::isfinite(value)) {
+ throw TMisuseException() << "Duration must be finite";
+ }
+ return defaultUnit(value);
+}
+
+TDuration ParseDurationMicroseconds(TStringBuf str) {
+ return ParseDurationWithDefaultUnit(str, [](double us) {
+ return TDuration::MicroSeconds(static_cast<ui64>(std::round(us)));
+ });
+}
+
+TDuration ParseDurationMilliseconds(TStringBuf str) {
+ return ParseDurationWithDefaultUnit(str, [](double ms) {
+ // Convert milliseconds to microseconds and round to preserve precision
+ return TDuration::MicroSeconds(static_cast<ui64>(std::round(ms * 1000.0)));
+ });
+}
+
+TDuration ParseDurationSeconds(TStringBuf str) {
+ return ParseDurationWithDefaultUnit(str, [](double sec) {
+ // Convert seconds to microseconds and round to preserve precision
+ return TDuration::MicroSeconds(static_cast<ui64>(std::round(sec * 1000000.0)));
+ });
+}
+
+TDuration ParseDurationHours(TStringBuf str) {
+ return ParseDurationWithDefaultUnit(str, [](double hours) {
+ // Convert hours to microseconds and round to preserve precision
+ return TDuration::MicroSeconds(static_cast<ui64>(std::round(hours * 3600000000.0)));
+ });
+}
+
+} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/common/duration.h b/ydb/public/lib/ydb_cli/common/duration.h
new file mode 100644
index 00000000000..73e44a67eb5
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/duration.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <ydb/public/lib/ydb_cli/common/command.h>
+#include <util/datetime/base.h>
+#include <util/string/strip.h>
+
+#include <cmath>
+
+namespace NYdb::NConsoleClient {
+
+// Parse duration with a default unit for backward compatibility
+// If the input is a plain number, it's interpreted using the defaultUnit
+// Otherwise, it's parsed as a duration string (e.g., "5s", "2m", "1h")
+// Supports fractional values: 0.5s = 500ms, 0.5m = 30s, 0.5h = 30m
+TDuration ParseDurationWithDefaultUnit(TStringBuf str, TDuration (*defaultUnit)(double));
+
+// Parse duration in microseconds with support for new format (e.g., "5s", "100ms", "500us")
+// If input is a plain number, it's interpreted as microseconds (for backward compatibility)
+// Supports fractional values: 0.5ms = 500us
+TDuration ParseDurationMicroseconds(TStringBuf str);
+
+// Parse duration in milliseconds with support for new format (e.g., "5s", "100ms")
+// If input is a plain number, it's interpreted as milliseconds (for backward compatibility)
+// Supports fractional values: 0.5s = 500ms
+TDuration ParseDurationMilliseconds(TStringBuf str);
+
+// Parse duration in seconds with support for new format (e.g., "5s", "100ms", "2m")
+// If input is a plain number, it's interpreted as seconds (for backward compatibility)
+// Supports fractional values: 0.5m = 30s
+TDuration ParseDurationSeconds(TStringBuf str);
+
+// Parse duration in hours with support for new format (e.g., "2h", "30m", "1d")
+// If input is a plain number, it's interpreted as hours (for backward compatibility)
+// Supports fractional values: 0.5h = 30m, 0.5d = 12h
+TDuration ParseDurationHours(TStringBuf str);
+
+// Parse duration requiring explicit unit (e.g., "5s", "2m", "1h")
+// Throws exception if no unit is specified
+TDuration ParseDuration(TStringBuf str);
+
+} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/common/duration_ut.cpp b/ydb/public/lib/ydb_cli/common/duration_ut.cpp
new file mode 100644
index 00000000000..051d849aeac
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/duration_ut.cpp
@@ -0,0 +1,135 @@
+#include <ydb/public/lib/ydb_cli/common/duration.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYdb::NConsoleClient;
+
+Y_UNIT_TEST_SUITE(ParseDurationTests) {
+ Y_UNIT_TEST(ParseDuration_WithUnit) {
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("5s"), TDuration::Seconds(5));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("100ms"), TDuration::MilliSeconds(100));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("2m"), TDuration::Minutes(2));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("1h"), TDuration::Hours(1));
+ }
+
+ Y_UNIT_TEST(ParseDuration_ThrowsOnPlainNumber) {
+ UNIT_ASSERT_EXCEPTION(ParseDuration("100"), TMisuseException);
+ }
+
+ Y_UNIT_TEST(ParseDuration_FractionalValues) {
+ // TDuration::Parse supports fractional values
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("0.5s"), TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("0.5m"), TDuration::Seconds(30));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("1.5h"), TDuration::Minutes(90));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDuration("0.5d"), TDuration::Hours(12));
+ }
+
+ Y_UNIT_TEST(ParseDurationMilliseconds_WithUnit) {
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("5s"), TDuration::Seconds(5));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("100ms"), TDuration::MilliSeconds(100));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("2m"), TDuration::Minutes(2));
+ }
+
+ Y_UNIT_TEST(ParseDurationMilliseconds_PlainNumber) {
+ // Plain number should be interpreted as milliseconds
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("1000"), TDuration::MilliSeconds(1000));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("500"), TDuration::MilliSeconds(500));
+ }
+
+ Y_UNIT_TEST(ParseDurationMilliseconds_FractionalPlainNumber) {
+ // Fractional plain number in milliseconds (rounds to microseconds)
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("500.5"), TDuration::MicroSeconds(500500));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("100.4"), TDuration::MicroSeconds(100400));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMilliseconds("1.0001"), TDuration::MicroSeconds(1000));
+ }
+
+ Y_UNIT_TEST(ParseDurationSeconds_WithUnit) {
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("5s"), TDuration::Seconds(5));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("100ms"), TDuration::MilliSeconds(100));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("2m"), TDuration::Minutes(2));
+ }
+
+ Y_UNIT_TEST(ParseDurationSeconds_PlainNumber) {
+ // Plain number should be interpreted as seconds
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("60"), TDuration::Seconds(60));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("30"), TDuration::Seconds(30));
+ }
+
+ Y_UNIT_TEST(ParseDurationSeconds_FractionalPlainNumber) {
+ // Fractional plain number in seconds (rounds to microseconds)
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("30.5"), TDuration::MicroSeconds(30500000));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("60.400001"), TDuration::MicroSeconds(60400001));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationSeconds("1.000001"), TDuration::MicroSeconds(1000001));
+ }
+
+ Y_UNIT_TEST(ParseDurationHours_WithUnit) {
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("2h"), TDuration::Hours(2));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("30m"), TDuration::Minutes(30));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("1d"), TDuration::Hours(24));
+ }
+
+ Y_UNIT_TEST(ParseDurationHours_PlainNumber) {
+ // Plain number should be interpreted as hours
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("2"), TDuration::Hours(2));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("24"), TDuration::Hours(24));
+ }
+
+ Y_UNIT_TEST(ParseDurationHours_FractionalPlainNumber) {
+ // Fractional plain number in hours (rounds to microseconds)
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("0.5"), TDuration::MicroSeconds(1800000000)); // 0.5h = 30 min = 1800000000 us
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("1.5"), TDuration::MicroSeconds(5400000000)); // 1.5h = 90 minutes = 5400000000 us
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationHours("0.000001"), TDuration::MicroSeconds(3600)); // 0.000001h = 3600 us = 3.6 ms
+ }
+
+ Y_UNIT_TEST(ParseDurationMicroseconds_WithUnit) {
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("5s"), TDuration::Seconds(5));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("100ms"), TDuration::MilliSeconds(100));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("500us"), TDuration::MicroSeconds(500));
+ }
+
+ Y_UNIT_TEST(ParseDurationMicroseconds_PlainNumber) {
+ // Plain number should be interpreted as microseconds
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("1000"), TDuration::MicroSeconds(1000));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("500"), TDuration::MicroSeconds(500));
+ }
+
+ Y_UNIT_TEST(ParseDurationMicroseconds_FractionalPlainNumber) {
+ // Fractional plain number in microseconds (should round)
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("500.5"), TDuration::MicroSeconds(501));
+ UNIT_ASSERT_VALUES_EQUAL(ParseDurationMicroseconds("100.4"), TDuration::MicroSeconds(100));
+ }
+
+ Y_UNIT_TEST(ParseDurationWithDefaultUnit_Hours) {
+ auto hoursParser = [](double hours) { return TDuration::Seconds(hours * 3600); };
+
+ // With unit - should use the unit
+ UNIT_ASSERT_VALUES_EQUAL(
+ ParseDurationWithDefaultUnit("1h", hoursParser),
+ TDuration::Hours(1)
+ );
+ UNIT_ASSERT_VALUES_EQUAL(
+ ParseDurationWithDefaultUnit("30m", hoursParser),
+ TDuration::Minutes(30)
+ );
+
+ // Plain number - should use default unit (hours)
+ UNIT_ASSERT_VALUES_EQUAL(
+ ParseDurationWithDefaultUnit("2", hoursParser),
+ TDuration::Hours(2)
+ );
+ UNIT_ASSERT_VALUES_EQUAL(
+ ParseDurationWithDefaultUnit("0.5", hoursParser),
+ TDuration::Minutes(30)
+ );
+ }
+
+ Y_UNIT_TEST(ParseDurationWithDefaultUnit_Negative) {
+ auto msParser = [](double ms) { return TDuration::MilliSeconds(static_cast<ui64>(ms)); };
+ UNIT_ASSERT_EXCEPTION(ParseDurationWithDefaultUnit("-5", msParser), TMisuseException);
+ }
+
+ Y_UNIT_TEST(ParseDurationWithDefaultUnit_Empty) {
+ auto msParser = [](double ms) { return TDuration::MilliSeconds(static_cast<ui64>(ms)); };
+ UNIT_ASSERT_EXCEPTION(ParseDurationWithDefaultUnit("", msParser), TMisuseException);
+ }
+}
diff --git a/ydb/public/lib/ydb_cli/common/ut/ya.make b/ydb/public/lib/ydb_cli/common/ut/ya.make
index f84667f9ee9..f8a851d0c97 100644
--- a/ydb/public/lib/ydb_cli/common/ut/ya.make
+++ b/ydb/public/lib/ydb_cli/common/ut/ya.make
@@ -3,6 +3,7 @@ UNITTEST_FOR(ydb/public/lib/ydb_cli/common)
SRCS(
cert_format_converter_ut.cpp
csv_parser_ut.cpp
+ duration_ut.cpp
normalize_path_ut.cpp
pg_dump_parser_ut.cpp
recursive_remove_ut.cpp
diff --git a/ydb/public/lib/ydb_cli/common/ya.make b/ydb/public/lib/ydb_cli/common/ya.make
index 1cc8047301e..93f2b6b98cb 100644
--- a/ydb/public/lib/ydb_cli/common/ya.make
+++ b/ydb/public/lib/ydb_cli/common/ya.make
@@ -10,6 +10,7 @@ SRCS(
common.cpp
csv_parser.cpp
describe.cpp
+ duration.cpp
examples.cpp
format.cpp
ftxui.cpp