Repository: apache/kvrocks
Pull request #2849: feat(tdigest): Add support for QUANTILE command
Author: login: SharonIV0x86
Created: 2025-03-23T07:19:43Z | Merged: 2025-05-16T16:24:52Z

Closes #2794 

This PR adds support for ``TDIGEST.QUANTILE`` command.

Linked issue #2794: TDigest: Implement QUANTILE command for TDigest Algorithm
### Search before asking

- [x] I had searched in the [issues](https://github.com/apache/kvrocks/issues) and found no similar issues.


### Motivation

Refer: #2558
We should implement `TDIGEST.QUANTILE` command.

Redis command reference:

https://redis.io/docs/latest/commands/tdigest.quantile/

### Solution

Depends on #2791 #2792 .

Follow the implementation of #2791 for this command.

Add go integration tests for it.

### Are you willing to submit a PR?

- [x] I'm willing to submit a PR!
Linked issue comments: user: login: AntiTopQuark
created_at: 2025-02-26T03:40:51Z
body: Hi, I would like to work on this command . Could you please assign the issue to me?
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-02-26T05:33:37Z
body: > Hi, I would like to work on this command . Could you please assign the issue to me?

Hi @AntiTopQuark ,

Thanks very much for your interests on this topic! 😁

This command depends on #2792 for integration tests.

Maybe we need to wait for this command to be finished.

Best Regards,
Edward

author_association: MEMBER
user: login: AntiTopQuark
created_at: 2025-02-26T12:54:15Z
body: OK, I will wait unitl #2792  is merged, and then I will take it on.
Could you assign me the issue beforehand?
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-02-26T13:27:15Z
body: > OK, I will wait unitl [#2792](https://github.com/apache/kvrocks/issues/2792) is merged, and then I will take it on. Could you assign me the issue beforehand?

Hi @AntiTopQuark ,

Thanks for your interest on this topic! You can leave an assignment comment here. 😊

You may need take care on the lock of quantile function.

We need just lock the merge part and keep read part read-only.

We should confirm that we create a correct lock with other write actions.

You can refer the comment in #2799 PR's description. 

Best Regards,
Edward
author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-03-19T09:58:20Z
body: @LindaSummer I was trying to work on this, but it seems like the code just freezes on this particular line when trying to lock the key before merging the unmerged nodes.
https://github.com/apache/kvrocks/blob/3e1b21b71dcb5d424c89204d05835a99e5d0fca3/src/types/redis_tdigest.cc#L193

Any guidance on this is appreciated.
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-03-19T13:01:00Z
body: > [@LindaSummer](https://github.com/LindaSummer?rgh-link-date=2025-03-19T09%3A58%3A20.000Z) I was trying to work on this, but it seems like the code just freezes on this particular line when trying to lock the key before merging the unmerged nodes.
> 
> [kvrocks/src/types/redis_tdigest.cc](https://github.com/apache/kvrocks/blob/3e1b21b71dcb5d424c89204d05835a99e5d0fca3/src/types/redis_tdigest.cc#L193)
> 
> Line 193 in [3e1b21b](/apache/kvrocks/commit/3e1b21b71dcb5d424c89204d05835a99e5d0fca3)
> 
>  LockGuard guard(storage_->GetLockManager(), ns_key); 
> Any guidance on this is appreciated.

Hi @SharonIV0x86 ,

You could refer to [the last comment above your comment](https://github.com/apache/kvrocks/issues/2794#issuecomment-2684957429) and comment in #2799 PR description.

We should only guard the merge part, for a fast testing, you can remove the lock guard and make this command a write command.
But it's just for self-testing, the final command should be a read command, and we should lock the merge part with correct key.

By the way, it will be better to confirm with @AntiTopQuark on his progress to reduce confliction. 😊

Best Regards,
Edward
author_association: MEMBER
user: login: AntiTopQuark
created_at: 2025-03-20T13:09:20Z
body: Hi @LindaSummer  @SharonIV0x86 

Thank you both for the follow-up and detailed explanations! Due to my current schedule constraints, I'm afraid I won't be able to dive into this issue in depth for now. My sincere apologies.

Thanks again for Edward's guidance, and best wishes to Sharon! ✨


author_association: CONTRIBUTOR
user: login: SharonIV0x86
created_at: 2025-03-21T12:29:44Z
body: @AntiTopQuark No issues, you can focus on your stuff.
I'll start working on it and open a PR, @LindaSummer If possibly kindly assign this to me.
Thanks.
author_association: CONTRIBUTOR

Diff:
diff --git a/src/commands/cmd_tdigest.cc b/src/commands/cmd_tdigest.cc
index 166a22afbdb..4c04e8619cd 100644
--- a/src/commands/cmd_tdigest.cc
+++ b/src/commands/cmd_tdigest.cc
@@ -18,11 +18,15 @@
  *
  */
 
+#include <range/v3/range/conversion.hpp>
+#include <range/v3/view/transform.hpp>
+
 #include "command_parser.h"
 #include "commander.h"
 #include "server/redis_reply.h"
 #include "server/server.h"
 #include "status.h"
+#include "string_util.h"
 #include "types/redis_tdigest.h"
 
 namespace redis {
@@ -37,6 +41,7 @@ constexpr auto kInfoMergedWeight = "Merged weight";
 constexpr auto kInfoUnmergedWeight = "Unmerged weight";
 constexpr auto kInfoObservations = "Observations";
 constexpr auto kInfoTotalCompressions = "Total compressions";
+constexpr auto kNan = "nan";
 }  // namespace
 
 class CommandTDigestCreate : public Commander {
@@ -242,11 +247,45 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    quantiles_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      quantiles_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, quantiles_, &result);
+    if (!s.ok()) {
+      if (s.IsNotFound()) {
+        return {Status::RedisExecErr, errKeyNotFound};
+      }
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    auto quantile_strings = result.quantiles
+                                ? (ranges::views::transform(*result.quantiles, util::Float2String) | ranges::to_vector)
+                                : std::vector<std::string>(quantiles_.size(), kNan);
+    *output = conn->MultiBulkString(quantile_strings);
+    return Status::OK();
+  }
 
+ private:
+  std::string key_name_;
+  std::vector<double> quantiles_;
+};
 REDIS_REGISTER_COMMANDS(TDigest, MakeCmdAttr<CommandTDigestCreate>("tdigest.create", -2, "write", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestInfo>("tdigest.info", 2, "read-only", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestAdd>("tdigest.add", -3, "write", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestMax>("tdigest.max", 2, "read-only", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestMin>("tdigest.min", 2, "read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandTDigestQuantile>("tdigest.quantile", -3, "read-only", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestReset>("tdigest.reset", 2, "write", 1, 1, 1));
 }  // namespace redis
diff --git a/src/types/redis_tdigest.cc b/src/types/redis_tdigest.cc
index a5a278b18f9..5f112a41095 100644
--- a/src/types/redis_tdigest.cc
+++ b/src/types/redis_tdigest.cc
@@ -196,6 +196,10 @@ rocksdb::Status TDigest::Quantile(engine::Context& ctx, const Slice& digest_name
       return status;
     }
 
+    if (metadata.total_observations == 0) {
+      return rocksdb::Status::OK();
+    }
+
     if (metadata.unmerged_nodes > 0) {
       auto batch = storage_->GetWriteBatchBase();
       WriteBatchLogData log_data(kRedisTDigest);
@@ -228,13 +232,17 @@ rocksdb::Status TDigest::Quantile(engine::Context& ctx, const Slice& digest_name
 
   auto dump_centroids = DummyCentroids(metadata, centroids);
 
+  auto quantile_results = std::vector<double>();
+  quantile_results.reserve(qs.size());
+
   for (auto q : qs) {
     auto status_or_value = TDigestQuantile(dump_centroids, q);
     if (!status_or_value) {
       return rocksdb::Status::InvalidArgument(status_or_value.Msg());
     }
-    result->quantiles.push_back(*status_or_value);
+    quantile_results.push_back(*status_or_value);
   }
+  result->quantiles = std::move(quantile_results);
 
   return rocksdb::Status::OK();
 }
diff --git a/src/types/redis_tdigest.h b/src/types/redis_tdigest.h
index 24ba650f211..b33a0387e1d 100644
--- a/src/types/redis_tdigest.h
+++ b/src/types/redis_tdigest.h
@@ -24,6 +24,7 @@
 #include <rocksdb/slice.h>
 #include <rocksdb/status.h>
 
+#include <optional>
 #include <vector>
 
 #include "storage/redis_db.h"
@@ -44,7 +45,7 @@ struct TDigestCreateOptions {
 };
 
 struct TDigestQuantitleResult {
-  std::vector<double> quantiles;
+  std::optional<std::vector<double>> quantiles;
 };
 
 class TDigest : public SubKeyScanner {
diff --git a/tests/cppunit/types/tdigest_test.cc b/tests/cppunit/types/tdigest_test.cc
index 994f7b1bcf9..91d1f311f4c 100644
--- a/tests/cppunit/types/tdigest_test.cc
+++ b/tests/cppunit/types/tdigest_test.cc
@@ -173,10 +173,11 @@ TEST_F(RedisTDigestTest, Quantile) {
   redis::TDigestQuantitleResult result;
   status = tdigest_->Quantile(*ctx_, test_digest_name, qs, &result);
   ASSERT_TRUE(status.ok()) << status.ToString();
-  ASSERT_EQ(result.quantiles.size(), qs.size());
-  EXPECT_NEAR(result.quantiles[0], 50.5, 0.01);
-  EXPECT_NEAR(result.quantiles[1], 90.5, 0.01);
-  EXPECT_NEAR(result.quantiles[2], 100, 0.01);
+  ASSERT_TRUE(result.quantiles);
+  ASSERT_EQ(result.quantiles->size(), qs.size());
+  EXPECT_NEAR((*result.quantiles)[0], 50.5, 0.01);
+  EXPECT_NEAR((*result.quantiles)[1], 90.5, 0.01);
+  EXPECT_NEAR((*result.quantiles)[2], 100, 0.01);
 }
 
 TEST_F(RedisTDigestTest, PlentyQuantile_10000_144) {
@@ -201,9 +202,10 @@ TEST_F(RedisTDigestTest, PlentyQuantile_10000_144) {
   redis::TDigestQuantitleResult tdigest_result;
   status = tdigest_->Quantile(*ctx_, test_digest_name, qs, &tdigest_result);
   ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(tdigest_result.quantiles);
 
   for (int i = 0; i < quantile_count; i++) {
-    EXPECT_NEAR(tdigest_result.quantiles[i], result[i], error_double) << "quantile is: " << qs[i];
+    EXPECT_NEAR((*tdigest_result.quantiles)[i], result[i], error_double) << "quantile is: " << qs[i];
   }
 }
 
@@ -234,10 +236,11 @@ TEST_F(RedisTDigestTest, Add_2_times) {
   redis::TDigestQuantitleResult tdigest_result;
   status = tdigest_->Quantile(*ctx_, test_digest_name, qs, &tdigest_result);
   ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(tdigest_result.quantiles);
 
   for (int i = 0; i < quantile_count; i++) {
     auto &[expect_down, expect_upper] = expect_result[i];
-    auto got = tdigest_result.quantiles[i];
+    auto got = (*tdigest_result.quantiles)[i];
     EXPECT_GE(got, expect_down) << fmt::format("quantile is {}, should in interval [{}, {}]", qs[i], expect_down,
                                                expect_upper);
     EXPECT_LE(got, expect_upper) << fmt::format("quantile is {}, should in interval [{}, {}]", qs[i], expect_down,
@@ -267,15 +270,31 @@ TEST_F(RedisTDigestTest, Add_100_times_same_value) {
   redis::TDigestQuantitleResult tdigest_result;
   status = tdigest_->Quantile(*ctx_, test_digest_name, qs, &tdigest_result);
   ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(tdigest_result.quantiles);
 
   auto expect_result = std::vector<double>{
       -10, -9, -8, -5, 1, 7, 10, 11, 12,
   };
 
-  EXPECT_EQ(tdigest_result.quantiles.size(), qs.size());
+  EXPECT_EQ(tdigest_result.quantiles->size(), qs.size());
 
   for (size_t i = 0; i < qs.size(); i++) {
-    auto got = tdigest_result.quantiles[i];
+    auto got = (*tdigest_result.quantiles)[i];
     EXPECT_NEAR(got, expect_result[i], 0.5) << fmt::format("quantile is {}, should be {}", qs[i], expect_result[i]);
   }
 }
+TEST_F(RedisTDigestTest, Quantile_returns_nan_on_empty_tdigest) {
+  std::string test_digest_name = "test_digest_nan" + std::to_string(util::GetTimeStampMS());
+
+  bool exists = false;
+  auto status = tdigest_->Create(*ctx_, test_digest_name, {100}, &exists);
+  ASSERT_FALSE(exists);
+  ASSERT_TRUE(status.ok());
+
+  std::vector<double> qs = {0.3, 0.1, 0.2, 0.56, 0.44, 0.12, 0.11};
+  redis::TDigestQuantitleResult result;
+
+  status = tdigest_->Quantile(*ctx_, test_digest_name, qs, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_FALSE(result.quantiles) << "should not have quantiles with empty tdigest";
+}
diff --git a/tests/gocase/unit/type/tdigest/tdigest_test.go b/tests/gocase/unit/type/tdigest/tdigest_test.go
index d3f11800405..62efacb9e1e 100644
--- a/tests/gocase/unit/type/tdigest/tdigest_test.go
+++ b/tests/gocase/unit/type/tdigest/tdigest_test.go
@@ -23,6 +23,7 @@ package tdigest
 
 import (
 	"context"
+	"strconv"
 	"testing"
 
 	"github.com/apache/kvrocks/tests/gocase/util"
@@ -310,4 +311,108 @@ func tdigestTests(t *testing.T, configs util.KvrocksServerConfigs) {
 		infoAfterEmptyReset := toTdigestInfo(t, rsp.Val())
 		require.EqualValues(t, 100, infoAfterEmptyReset.Compression)
 	})
+	t.Run("tdigest.quantile with different arguments", func(t *testing.T) {
+		keyPrefix := "t_qt_"
+
+		// No arguments
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE").Err(), errMsgWrongNumberArg)
+
+		// Non-existent key
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE", keyPrefix+"iDoNotExist", "0.5").Err(), errMsgKeyNotExist)
+
+		{
+			// Quantile on empry tdigest
+			key0 := keyPrefix + "00"
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key0, "compression", "100").Err())
+			rsp := rdb.Do(ctx, "TDIGEST.QUANTILE", key0, "0.4", "0.1", "0.2", "0.3", "0.7", "0.5")
+			require.NoError(t, rsp.Err())
+			vals, err := rsp.Slice()
+			require.NoError(t, err)
+			require.Equal(t, 6, len(vals))
+			for _, v := range vals {
+				s, ok := v.(string)
+				require.True(t, ok, "Expected string but got %T", v)
+				require.Equal(t, "nan", s, "Expected value to be 'nan'")
+			}
+		}
+		{
+			// Quantiles on positive data
+			key1 := keyPrefix + "01"
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key1, "compression", "100").Err())
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key1, "1", "2", "2", "3", "3", "3", "4", "4", "4", "4", "5", "5", "5", "5", "5").Err())
+			rsp := rdb.Do(ctx, "TDIGEST.QUANTILE", key1, "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1")
+			require.NoError(t, rsp.Err())
+			vals, err := rsp.Slice()
+			require.NoError(t, err)
+			require.Len(t, vals, 11)
+			expected := []float64{1.0, 2.0, 2.5, 3.0, 3.5, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0}
+			for i, v := range vals {
+				str, ok := v.(string)
+				require.True(t, ok, "expected string but got %T at index %d", v, i)
+
+				got, err := strconv.ParseFloat(str, 64)
+				require.NoError(t, err, "could not parse value at index %d", i)
+
+				require.InEpsilon(t, expected[i], got, 0.0001, "mismatch at index %d", i)
+			}
+		}
+		{
+			// Quantiles on negative data
+			key2 := keyPrefix + "02"
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key2, "compression", "100").Err())
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key2, "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10").Err())
+			rsp := rdb.Do(ctx, "TDIGEST.QUANTILE", key2, "0", "0.25", "0.5", "0.75", "1")
+			require.NoError(t, rsp.Err())
+
+			vals, err := rsp.Slice()
+			require.NoError(t, err)
+			require.Len(t, vals, 5)
+
+			expected := []float64{-10.0, -8.0, -5.5, -3.0, -1.0}
+			for i, v := range vals {
+				str, ok := v.(string)
+				require.True(t, ok, "expected string but got %T at index %d", v, i)
+
+				got, err := strconv.ParseFloat(str, 64)
+				require.NoError(t, err, "could not parse value at index %d", i)
+
+				require.InEpsilon(t, expected[i], got, 0.0001, "mismatch at index %d", i)
+			}
+		}
+		{
+			// Query with unordered quantiles
+			key3 := keyPrefix + "03"
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key3, "compression", "100").Err())
+			require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key3,
+				"3", "12", "-3", "-19", "13", "4", "14", "18", "-1", "-5", "15", "-10", "33", "17", "-20",
+			).Err())
+			rsp := rdb.Do(ctx, "TDIGEST.QUANTILE", key3,
+				"0.9", "0.1", "0.7", "0.3", "0.6", "0.0", "0.55", "0.65", "0.34", "0.88",
+			)
+			require.NoError(t, rsp.Err())
+			vals, err := rsp.Slice()
+			require.NoError(t, err)
+			require.Equal(t, 10, len(vals))
+
+			expected := []float64{
+				18.0,
+				-19.0,
+				14.0,
+				-3.0,
+				12.5,
+				-20.0,
+				12.0,
+				13.0,
+				-1.0,
+				18.0,
+			}
+			for i, v := range vals {
+				strVal, ok := v.(string)
+				require.True(t, ok, "Expected string at index %d but got %T", i, v)
+				numVal, err := strconv.ParseFloat(strVal, 64)
+				require.NoError(t, err, "Failed to parse value at index %d: %s", i, strVal)
+				require.InDelta(t, expected[i], numVal, 1e-6, "Mismatch at index %d", i)
+			}
+		}
+	})
 }


Reviews:
user: login: danishgh
state: APPROVED
author_association: NONE
submitted_at: 2025-03-30T18:19:01Z
user: login: PragmaTwice
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T02:24:57Z
user: login: PragmaTwice
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T02:25:30Z
user: login: PragmaTwice
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T06:35:20Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:37:54Z
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Sorry for delay review. 😊

For the lock of quantile merging, I have checked the lock in `redis_connection.cc` and `Database::AppendNamespacePrefix`.

They lock the same key for same tdigest user key.

So, it is safe to lock the [`ns_key`](https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/types/redis_tdigest.cc#L190-L193).

https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/server/redis_connection.cc#L521

https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/storage/redis_db.cc#L520-L522

Best Regards,
Edward
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T04:16:55Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:53:51Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:54:04Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:05:22Z
user: login: LindaSummer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-03T03:44:47Z
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Thanks very much for your effort. 😊
Left some comments.

Best Regards,
Edward
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-03T08:02:27Z
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Thanks very much for your effort! 😊

Left some comments.

Best Regards,
Edward
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:14:55Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:28:30Z
user: login: SharonIV0x86
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:29:07Z
user: login: LindaSummer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:41:12Z
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Thanks very much for your contribution and huge effort! 😄
I have made some small changes on current code.

LGTM.

Best Regards,
Edward

state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T15:04:39Z
user: login: PragmaTwice
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T16:24:39Z

Review comments:
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
+    if (!s.ok()) {
+      if (s.IsNotFound()) {
+        return {Status::RedisExecErr, errKeyNotFound};
+      }
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    std::vector<std::string> quantile_strings;
+    quantile_strings.reserve(result.quantiles.size());
+    for (const auto &q : result.quantiles) {
+      quantile_strings.push_back(std::to_string(q));
+    }
+    *output = redis::MultiBulkString(RESP::v2, quantile_strings);
path: src/commands/cmd_tdigest.cc
user: login: PragmaTwice
body: ```suggestion
    *output = conn->MultiBulkString(quantile_strings);
```
created_at: 2025-04-21T02:24:57Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) {
path: src/commands/cmd_tdigest.cc
user: login: PragmaTwice
body: please add `override` to them, as reported in CI.
created_at: 2025-04-21T02:25:30Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) {
path: src/commands/cmd_tdigest.cc
user: login: PragmaTwice
body: `override` also here. You can check CI logs to know this.
![image](https://github.com/user-attachments/assets/be3edc42-b47e-46d5-9319-4961deaaa5d8)

created_at: 2025-04-21T06:35:20Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) {
path: src/commands/cmd_tdigest.cc
user: login: SharonIV0x86
body: mb, ill fix it.
created_at: 2025-04-21T06:37:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
+    if (!s.ok()) {
+      if (s.IsNotFound()) {
+        return {Status::RedisExecErr, errKeyNotFound};
+      }
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    std::vector<std::string> quantile_strings;
+    quantile_strings.reserve(result.quantiles.size());
+    for (const auto &q : result.quantiles) {
+      quantile_strings.push_back(std::to_string(q));
+    }
+    *output = conn->MultiBulkString(quantile_strings);
+    return Status::OK();
+  }
 
+ private:
+  std::string key_name_;
+  std::vector<double> values_;
+};
 REDIS_REGISTER_COMMANDS(TDigest, MakeCmdAttr<CommandTDigestCreate>("tdigest.create", -2, "write", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestInfo>("tdigest.info", 2, "read-only", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestAdd>("tdigest.add", -3, "write", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestMax>("tdigest.max", 2, "read-only", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestMin>("tdigest.min", 2, "read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandTDigestQuantile>("tdigest.quantile", -2, "read-only", 1, 1, 1),
path: src/commands/cmd_tdigest.cc
user: login: LindaSummer
body: ```suggestion
                        MakeCmdAttr<CommandTDigestQuantile>("tdigest.quantile", -3, "read-only", 1, 1, 1),
```
created_at: 2025-05-02T04:04:32Z
author_association: MEMBER
diff_hunk: @@ -310,4 +311,43 @@ func tdigestTests(t *testing.T, configs util.KvrocksServerConfigs) {
 		infoAfterEmptyReset := toTdigestInfo(t, rsp.Val())
 		require.EqualValues(t, 100, infoAfterEmptyReset.Compression)
 	})
+	t.Run("tdigest.quantile with different arguments", func(t *testing.T) {
+		keyPrefix := "t_qt_"
+
+		//Testing with no arguments
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE").Err(), errMsgWrongNumberArg)
+
+		// Quantile on a non existent key
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE", keyPrefix+"iDoNotExist").Err(), errMsgKeyNotExist)
+
+		// Creating a key
+		require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", keyPrefix+"01", "compression", "100").Err())
+
+		key := keyPrefix + "01"
+		//Adding some data to tdigest 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5
+		require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", "2", "3", "3", "3", "4", "4", "4", "4", "5", "5", "5", "5", "5").Err())
path: tests/gocase/unit/type/tdigest/tdigest_test.go
user: login: LindaSummer
body: Maybe we could add some unordered inputs with some negative numbers as test cases. 😊
created_at: 2025-05-02T04:06:31Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
path: src/commands/cmd_tdigest.cc
user: login: LindaSummer
body: We should handle the case for an empty tdigest in `TDigest::Quantile`.
Cpp unit test cases should also be adapted after we handle it in `TDigest::Quantile`.

Here is my test case in redis stack.
```
127.0.0.1:6389> flushall
OK
127.0.0.1:6389> tdigest.create hello
OK
127.0.0.1:6389> tdigest.quantile hello 0.1
1) "nan"
127.0.0.1:6389>
```

Here is the output of current implementation.
```
127.0.0.1:6666> flushall
OK
127.0.0.1:6666> tdigest.create hello
OK
127.0.0.1:6666> tdigest.quantile hello 0.1
(error) ERR Invalid argument: invalid quantile or empty tdigest
```
created_at: 2025-05-02T04:11:07Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
path: src/commands/cmd_tdigest.cc
user: login: SharonIV0x86
body: Sorry for delayed response i was busy with an event. About this yes, i have added this check locally but i will just push it in sometime.
created_at: 2025-05-02T13:53:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -310,4 +311,43 @@ func tdigestTests(t *testing.T, configs util.KvrocksServerConfigs) {
 		infoAfterEmptyReset := toTdigestInfo(t, rsp.Val())
 		require.EqualValues(t, 100, infoAfterEmptyReset.Compression)
 	})
+	t.Run("tdigest.quantile with different arguments", func(t *testing.T) {
+		keyPrefix := "t_qt_"
+
+		//Testing with no arguments
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE").Err(), errMsgWrongNumberArg)
+
+		// Quantile on a non existent key
+		require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.QUANTILE", keyPrefix+"iDoNotExist").Err(), errMsgKeyNotExist)
+
+		// Creating a key
+		require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", keyPrefix+"01", "compression", "100").Err())
+
+		key := keyPrefix + "01"
+		//Adding some data to tdigest 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5
+		require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", "2", "3", "3", "3", "4", "4", "4", "4", "5", "5", "5", "5", "5").Err())
path: tests/gocase/unit/type/tdigest/tdigest_test.go
user: login: SharonIV0x86
body: Sure will do that.
created_at: 2025-05-02T13:54:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
path: src/commands/cmd_tdigest.cc
user: login: SharonIV0x86
body: I can add another error constant for this in ``error_constants.h`` file if that works? Because we can use the same one for CDF, Rank and etc commands as well in future.
created_at: 2025-05-02T16:05:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,11 +242,47 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
path: src/commands/cmd_tdigest.cc
user: login: LindaSummer
body: > I can add another error constant for this in `error_constants.h` file if that works? Because we can use the same one for CDF, Rank and etc commands as well in future.

Hi @SharonIV0x86 ,

For my side, I think the "nan" is not an error and defining a literal string inside tdigest scope may be better. 😊

Best Regards,
Edward
created_at: 2025-05-03T03:44:47Z
author_association: MEMBER
diff_hunk: @@ -242,11 +242,50 @@ class CommandTDigestMax : public CommandTDigestMinMax {
  public:
   CommandTDigestMax() : CommandTDigestMinMax(false) {}
 };
+class CommandTDigestQuantile : public Commander {
+  Status Parse(const std::vector<std::string> &args) override {
+    key_name_ = args[1];
+    values_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); i++) {
+      auto value = ParseFloat(args[i]);
+      if (!value) {
+        return {Status::RedisParseErr, errValueIsNotFloat};
+      }
+      values_.push_back(*value);
+    }
+    return Status::OK();
+  }
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestQuantitleResult result;
+    auto s = tdigest.Quantile(ctx, key_name_, values_, &result);
+    if (!s.ok()) {
+      if (s.IsNotFound()) {
+        return {Status::RedisExecErr, errKeyNotFound};
+      }
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    if (values_.empty()) {
+      return {Status::RedisExecErr, "invalid quantile or empty tdigest"};
path: src/commands/cmd_tdigest.cc
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Sorry for my unclear description for this behavior. 😄

Currently, redis stack returns nan as response for none centroids.
We'd better follow this behavior.

Adding the logic to tdigest ranther than command may be better to keep the command module clean.

After modifying the tdigest, the cpp unit test should also be updated.

Best Regards,
Edward
created_at: 2025-05-03T07:52:40Z
author_association: MEMBER
diff_hunk: @@ -310,4 +311,66 @@ func tdigestTests(t *testing.T, configs util.KvrocksServerConfigs) {
 		infoAfterEmptyReset := toTdigestInfo(t, rsp.Val())
 		require.EqualValues(t, 100, infoAfterEmptyReset.Compression)
 	})
+	t.Run("tdigest.quantile with different arguments", func(t *testing.T) {
path: tests/gocase/unit/type/tdigest/tdigest_test.go
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

We could also add an unordered sequence and an empty tdigest as cases. 😊

Best Regards,
Edward
created_at: 2025-05-03T08:01:19Z
author_association: MEMBER
diff_hunk: @@ -265,13 +265,16 @@ class CommandTDigestQuantile : public Commander {
       }
       return {Status::RedisExecErr, s.ToString()};
     }
-    if (values_.empty()) {
-      return {Status::RedisExecErr, "invalid quantile or empty tdigest"};
-    }
     std::vector<std::string> quantile_strings;
     quantile_strings.reserve(result.quantiles.size());
-    for (const auto &q : result.quantiles) {
-      quantile_strings.push_back(std::to_string(q));
+    if (!result.has_centroids) {
+      for (size_t i = 0; i < values_.size(); ++i) {
+        quantile_strings.emplace_back("nan");
path: src/commands/cmd_tdigest.cc
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

Maybe we could make "nan" a constexpr literal string in this file. 😊
It would also be used in other commands.

Best Regards,
Edward
created_at: 2025-05-08T13:11:17Z
author_association: MEMBER
diff_hunk: @@ -225,6 +225,10 @@ rocksdb::Status TDigest::Quantile(engine::Context& ctx, const Slice& digest_name
   if (auto status = dumpCentroids(ctx, ns_key, metadata, &centroids); !status.ok()) {
     return status;
   }
+  if (centroids.empty()) {
path: src/types/redis_tdigest.cc
user: login: LindaSummer
body: Hi @SharonIV0x86 ,

At the first time we load the metadata, we should know this tdigest has no data inside it.
This could be checked before the centroids retrieving.

And it would be better to add cpp unit test for this behavior.

Best Regards,
Edward

created_at: 2025-05-08T13:13:50Z
author_association: MEMBER
diff_hunk: @@ -225,6 +225,10 @@ rocksdb::Status TDigest::Quantile(engine::Context& ctx, const Slice& digest_name
   if (auto status = dumpCentroids(ctx, ns_key, metadata, &centroids); !status.ok()) {
     return status;
   }
+  if (centroids.empty()) {
path: src/types/redis_tdigest.cc
user: login: SharonIV0x86
body: So just to be clear, after we load the metadata [here](https://github.com/apache/kvrocks/blob/6719b607141fd2b07f5d5a967971fc1d6de0f87a/src/types/redis_tdigest.cc#L191C19-L191C27) i add a check to do something like this.

```cpp
if(metadata.merged_nodes == 0 && metadata.unmerged_nodes == 0){
  //tdigest does not contain any data. so set the flag
}
```
Correct me if im wrong.

> And it would be better to add cpp unit test for this behavior.

I'll try working on this as well.
created_at: 2025-05-08T13:28:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -265,13 +265,16 @@ class CommandTDigestQuantile : public Commander {
       }
       return {Status::RedisExecErr, s.ToString()};
     }
-    if (values_.empty()) {
-      return {Status::RedisExecErr, "invalid quantile or empty tdigest"};
-    }
     std::vector<std::string> quantile_strings;
     quantile_strings.reserve(result.quantiles.size());
-    for (const auto &q : result.quantiles) {
-      quantile_strings.push_back(std::to_string(q));
+    if (!result.has_centroids) {
+      for (size_t i = 0; i < values_.size(); ++i) {
+        quantile_strings.emplace_back("nan");
path: src/commands/cmd_tdigest.cc
user: login: SharonIV0x86
body: Good idea, will do this.
created_at: 2025-05-08T13:29:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +225,10 @@ rocksdb::Status TDigest::Quantile(engine::Context& ctx, const Slice& digest_name
   if (auto status = dumpCentroids(ctx, ns_key, metadata, &centroids); !status.ok()) {
     return status;
   }
+  if (centroids.empty()) {
path: src/types/redis_tdigest.cc
user: login: LindaSummer
body: > So just to be clear, after we load the metadata [here](https://github.com/apache/kvrocks/blob/6719b607141fd2b07f5d5a967971fc1d6de0f87a/src/types/redis_tdigest.cc#L191C19-L191C27) i add a check to do something like this.
> 
> 
> 
> ```cpp
> 
> if(metadata.merged_nodes == 0 && metadata.unmerged_nodes == 0){
> 
>   //tdigest does not contain any data. so set the flag
> 
> }
> 
> ```
> 
> Correct me if im wrong.
> 
> 
> 
> > And it would be better to add cpp unit test for this behavior.
> 
> 
> 
> I'll try working on this as well.

Hi @SharonIV0x86 ,

We could directly use `total_observations` to check the tdigest's elements. 😊

Best Regards,
Edward
created_at: 2025-05-09T03:41:12Z
author_association: MEMBER

PR comments:
user: login: SharonIV0x86
created_at: 2025-03-30T18:25:43Z
body: @LindaSummer I have added the basic structure for the ``QUANTILE`` command but as you mentioned in the issue, there seems to be a problem with the locking mechanism or something else.


```
E20250330 23:50:42.491079 130596879718080 redis_tdigest.cc:524] metadata has 24 merged nodes, but got 4
```

I tried modifying the quantile function as mentioned in the issue and the previous PRs but the issue persists. Any help is appreciated.
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-04-01T03:42:09Z
body: Hi @SharonIV0x86 ,

Thanks very much for effort!😊

I'll try to test it in local in recent days and will sync if there is any new updates.

Best Regards,
Edward

author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-04-01T10:26:50Z
body: > Hi @SharonIV0x86 ,
> 
> Thanks very much for effort!😊
> 
> I'll try to test it in local in recent days and will sync if there is any new updates.
> 
> Best Regards, Edward

Works, till then ill try to fiddle around. Cheers.
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-04-08T14:12:07Z
body: Hi @SharonIV0x86 ,

Sorry for delay of updating. 😊

In recent I have some personal affairs which occupied my schedule so maybe a little delay in response.

I'm now working on this ticket and find that there may be some bugs inside it. I will double confirm it and create a PR to fix it if bug exists.

Best Regards,
Edward

author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-04-08T14:39:28Z
body: > Hi @SharonIV0x86 ,
> 
> Sorry for delay of updating. 😊
> 
> In recent I have some personal affairs which occupied my schedule so maybe a little delay in response.
> 
> I'm now working on this ticket and find that there may be some bugs inside it. I will double confirm it and create a PR to fix it if bug exists.
> 
> Best Regards, Edward

No issues, meanwhile i tried looking into it, no matter what i did there seems to be a mismatch between the actual information and the tdigest metadata now one thing i can deduce is that is not in the Quantile function but this seems to be the issue with ``mergeCurrentBuffer`` function or some other centroids function being called from the Quantile function.

Or this is related to locks as you suggested. 

There is no hurry from my side in fixing this issue but i am also planning to implement the [TDIGEST.CDF](https://github.com/apache/kvrocks/issues/2807) and [TDIGEST.RANK](https://redis.io/docs/latest/commands/tdigest.rank/) in upcoming weeks and they most likely will depend in this issue.
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-04-13T13:01:04Z
body: Hi @SharonIV0x86 ,

I have tested the command line successfully with removing the lock and announcing the command "write" for quick validation after fixing #2878 .

I will try to find a way to improve the performance with less critical section.

Best Regards,
Edward
author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-04-13T13:16:57Z
body: > Hi @SharonIV0x86 ,
> 
> I have tested the command line successfully with removing the lock and announcing the command "write" for quick validation after fixing #2878 .
> 
> I will try to find a way to improve the performance with less critical section.
> 
> Best Regards, Edward

Okay, thanks for looking into it. So, how should i proceed then? should i wait for #2878 to be merged?
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-04-13T13:25:19Z
body: > > Hi @SharonIV0x86 ,
> > I have tested the command line successfully with removing the lock and announcing the command "write" for quick validation after fixing #2878 .
> > I will try to find a way to improve the performance with less critical section.
> > Best Regards, Edward
> 
> Okay, thanks for looking into it. So, how should i proceed then? should i wait for #2878 to be merged?

Hi @SharonIV0x86 ,

Maybe we could wait for it to be merged before this PR.
It will affect our go integration test cases.

If you don't mind, I will try to solve the lock issue since I find that it may not be so easy to just add a lock key in current connection lock management.
After this PR solved, we could follow it for other commands like `CDF`.

Best Regards,
Edward

author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-04-13T13:30:00Z
body: 
> Hi @SharonIV0x86 ,
> 
> Maybe we could wait for it to be merged before this PR. It will affect our go integration test cases.
> 
> If you don't mind, I will try to solve the lock issue since I find that it may not be so easy to just add a lock key in current connection lock management. After this PR solved, we could follow it for other commands like `CDF`.
> 
> Best Regards, Edward

Absolutely no issues whatsoever, there is no hurry i am happy to wait 😄 
Let me know if there’s anything I can help with in the meantime.
author_association: CONTRIBUTOR
user: login: SharonIV0x86
created_at: 2025-04-16T11:23:06Z
body: @LindaSummer Hi, i have tested the quantile command after the merging of the #2878 and it works as expected.

However, there is a bit of difference in quantile values when compared to the original implementation of [ redis ``tdigest.quantile``](https://redis.io/docs/latest/commands/tdigest.quantile/) but i believe kvrocks quantile is more accurate as it uses linear interpolation so it should be good.

I have also added a go test case so if any changes are required pls lmk. 👍🏼 
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-04-28T12:18:09Z
body: Hi @SharonIV0x86 ,

I'm very sorry for delay in recent. 

My baby was born in recent, so I need to support my family these days.

Maybe I will be delay in response. I will go through this problem in this week as soon as I could.

Best Regards,
Edward
author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-04-28T12:47:29Z
body: > Hi @SharonIV0x86 ,
> 
> I'm very sorry for delay in recent.
> 
> My baby was born in recent, so I need to support my family these days.
> 
> Maybe I will be delay in response. I will go through this problem in this week as soon as I could.
> 
> Best Regards, Edward

Absolutely no issues man take your time, there is no hurry with this and congrats btw👏🏼. 
The PRs and issues can always wait you should give time to your family right now.
author_association: CONTRIBUTOR
user: login: SharonIV0x86
created_at: 2025-05-02T14:00:18Z
body: > Hi @SharonIV0x86 ,
> 
> Sorry for delay review. 😊
> 
> For the lock of quantile merging, I have checked the lock in `redis_connection.cc` and `Database::AppendNamespacePrefix`.
> 
> They lock the same key for same tdigest user key.
> 
> So, it is safe to lock the [`ns_key`](https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/types/redis_tdigest.cc#L190-L193).
> 
> https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/server/redis_connection.cc#L521
> 
> https://github.com/apache/kvrocks/blob/009a1bd80bdde88c091a69aca1539be0f58d2658/src/storage/redis_db.cc#L520-L522
> 
> Best Regards, Edward

Understood. So the current logic is fine and we can most probably refactor it into a function later on #2907 
author_association: CONTRIBUTOR
user: login: PragmaTwice
created_at: 2025-05-08T05:00:30Z
body: @LindaSummer Would you like to have a look?
author_association: MEMBER
user: login: LindaSummer
created_at: 2025-05-08T05:26:25Z
body: > @LindaSummer Would you like to have a look?

Hi @PragmaTwice and @SharonIV0x86 ,

Of course!😊

I will go through this PR later today.

Best Regards,
Edward
author_association: MEMBER
user: login: SharonIV0x86
created_at: 2025-05-09T07:42:35Z
body: @LindaSummer I have added the cpp test case, i believe it needs some modifications because in the cpp test case its currently not possible to directly compare the values returned by the quantile function with the "nan" string as the ``result.quantile`` vector is returned empty if the tdigest is empty. The behaviors differ in the command implementation.

The closest i could do was to add a assert for the value of ``result.has_centroids`` and it should be true and if its found true then it can be assumed that in the ``Execute`` function of the command implementation will construct the "nan" strings vector and will return it as a response. 
author_association: CONTRIBUTOR
user: login: LindaSummer
created_at: 2025-05-09T07:53:10Z
body: > @LindaSummer I have added the cpp test case, i believe it needs some modifications because in the cpp test case its currently not possible to directly compare the values returned by the quantile function with the "nan" string as the ``result.quantile`` vector is returned empty if the tdigest is empty. The behaviors differ in the command implementation.
> 
> 
> 
> The closest i could do was to add a assert for the value of ``result.has_centroids`` and it should be true and if its found true then it can be assumed that in the ``Execute`` function of the command implementation will construct the "nan" strings vector and will return it as a response. 

Hi @SharonIV0x86 ,

You could directly test the flag you have added. 😊
If you want a more clear semantic of the result, maybe `std::optional<std::vector<double>` is a choice.

And the nan string test could be in integration test.

Best Regards,
Edward
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-05-16T16:24:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=apache_kvrocks&pullRequest=2849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=apache_kvrocks&pullRequest=2849&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=apache_kvrocks&pullRequest=2849&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=apache_kvrocks&pullRequest=2849&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [61.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=apache_kvrocks&pullRequest=2849&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=apache_kvrocks&pullRequest=2849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=apache_kvrocks&pullRequest=2849)


author_association: NONE
user: login: SharonIV0x86
created_at: 2025-05-17T05:32:12Z
body: > Hi @SharonIV0x86 ,
> 
> Thanks very much for your contribution and huge effort! 😄 I have made some small changes on current code.
> 
> LGTM.
> 
> Best Regards, Edward

Thanks for the changes you made, the code is much better now with ranges and views. 
I have opened a PR for the quantile command in kvrocks-website https://github.com/apache/kvrocks-website/pull/300
author_association: CONTRIBUTOR