This is an automated email from the ASF dual-hosted git repository.
twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 8a4457ae Add the support of the LCS command (#2116)
8a4457ae is described below
commit 8a4457aeb3592d2961c97e6456c09c32ea4620f4
Author: Jover <[email protected]>
AuthorDate: Sun Mar 3 20:45:53 2024 +0800
Add the support of the LCS command (#2116)
Co-authored-by: 纪华裕 <[email protected]>
Co-authored-by: hulk <[email protected]>
---
src/commands/cmd_string.cc | 86 +++++++++++++-
src/server/redis_request.cc | 4 -
src/server/redis_request.h | 4 +
src/types/redis_string.cc | 153 +++++++++++++++++++++++++
src/types/redis_string.h | 33 +++++-
tests/cppunit/types/string_test.cc | 51 +++++++++
tests/gocase/unit/type/strings/strings_test.go | 104 +++++++++++++++++
7 files changed, 428 insertions(+), 7 deletions(-)
diff --git a/src/commands/cmd_string.cc b/src/commands/cmd_string.cc
index 3f7b5090..ad5a6bf5 100644
--- a/src/commands/cmd_string.cc
+++ b/src/commands/cmd_string.cc
@@ -26,6 +26,7 @@
#include "commands/command_parser.h"
#include "error_constants.h"
#include "server/redis_reply.h"
+#include "server/redis_request.h"
#include "server/server.h"
#include "storage/redis_db.h"
#include "time_util.h"
@@ -620,6 +621,88 @@ class CommandCAD : public Commander {
}
};
+class CommandLCS : public Commander {
+ public:
+ Status Parse(const std::vector<std::string> &args) override {
+ CommandParser parser(args, 3);
+ bool get_idx = false;
+ bool get_len = false;
+ while (parser.Good()) {
+ if (parser.EatEqICase("IDX")) {
+ get_idx = true;
+ } else if (parser.EatEqICase("LEN")) {
+ get_len = true;
+ } else if (parser.EatEqICase("WITHMATCHLEN")) {
+ with_match_len_ = true;
+ } else if (parser.EatEqICase("MINMATCHLEN")) {
+ min_match_len_ = GET_OR_RET(parser.TakeInt<int64_t>());
+ if (min_match_len_ < 0) {
+ min_match_len_ = 0;
+ }
+ } else {
+ return parser.InvalidSyntax();
+ }
+ }
+
+ // Complain if the user passed ambiguous parameters.
+ if (get_idx && get_len) {
+ return {Status::RedisParseErr,
+ "If you want both the length and indexes, "
+ "please just use IDX."};
+ }
+
+ if (get_len) {
+ type_ = StringLCSType::LEN;
+ } else if (get_idx) {
+ type_ = StringLCSType::IDX;
+ }
+
+ return Status::OK();
+ }
+
+ Status Execute(Server *srv, Connection *conn, std::string *output) override {
+ redis::String string_db(srv->storage, conn->GetNamespace());
+
+ StringLCSResult rst;
+ auto s = string_db.LCS(args_[1], args_[2], {type_, min_match_len_}, &rst);
+ if (!s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ // Build output by the rst type.
+ if (auto lcs = std::get_if<std::string>(&rst)) {
+ *output = redis::BulkString(*lcs);
+ } else if (auto len = std::get_if<uint32_t>(&rst)) {
+ *output = redis::Integer(*len);
+ } else if (auto result = std::get_if<StringLCSIdxResult>(&rst)) {
+ *output = conn->HeaderOfMap(2);
+ *output += redis::BulkString("matches");
+ *output += redis::MultiLen(result->matches.size());
+ for (const auto &match : result->matches) {
+ *output += redis::MultiLen(with_match_len_ ? 3 : 2);
+ *output += redis::MultiLen(2);
+ *output += redis::Integer(match.a.start);
+ *output += redis::Integer(match.a.end);
+ *output += redis::MultiLen(2);
+ *output += redis::Integer(match.b.start);
+ *output += redis::Integer(match.b.end);
+ if (with_match_len_) {
+ *output += redis::Integer(match.match_len);
+ }
+ }
+ *output += redis::BulkString("len");
+ *output += redis::Integer(result->len);
+ }
+
+ return Status::OK();
+ }
+
+ private:
+ StringLCSType type_ = StringLCSType::NONE;
+ bool with_match_len_ = false;
+ int64_t min_match_len_ = 0;
+};
+
REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandGet>("get", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGetEx>("getex", -2, "write", 1, 1, 1),
MakeCmdAttr<CommandStrlen>("strlen", 2, "read-only", 1, 1, 1),
@@ -637,6 +720,5 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandIncrByFloat>("incrbyfloat", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandIncr>("incr", 2, "write", 1, 1, 1),
MakeCmdAttr<CommandDecrBy>("decrby", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandDecr>("decr", 2, "write", 1, 1, 1),
MakeCmdAttr<CommandCAS>("cas", -4, "write", 1, 1, 1),
- MakeCmdAttr<CommandCAD>("cad", 3, "write", 1, 1, 1), )
-
+ MakeCmdAttr<CommandCAD>("cad", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandLCS>("lcs", -3, "read-only", 1, 2, 1), )
} // namespace redis
diff --git a/src/server/redis_request.cc b/src/server/redis_request.cc
index 4c796b41..32aee34d 100644
--- a/src/server/redis_request.cc
+++ b/src/server/redis_request.cc
@@ -36,10 +36,6 @@
namespace redis {
-const size_t PROTO_INLINE_MAX_SIZE = 16 * 1024L;
-const size_t PROTO_BULK_MAX_SIZE = 512 * 1024L * 1024L;
-const size_t PROTO_MULTI_MAX_SIZE = 1024 * 1024L;
-
Status Request::Tokenize(evbuffer *input) {
size_t pipeline_size = 0;
diff --git a/src/server/redis_request.h b/src/server/redis_request.h
index 0734cd9e..bb2d0554 100644
--- a/src/server/redis_request.h
+++ b/src/server/redis_request.h
@@ -32,6 +32,10 @@ class Server;
namespace redis {
+constexpr size_t PROTO_INLINE_MAX_SIZE = 16 * 1024L;
+constexpr size_t PROTO_BULK_MAX_SIZE = 512 * 1024L * 1024L;
+constexpr size_t PROTO_MULTI_MAX_SIZE = 1024 * 1024L;
+
using CommandTokens = std::vector<std::string>;
class Connection;
diff --git a/src/types/redis_string.cc b/src/types/redis_string.cc
index 7178311a..38df40b5 100644
--- a/src/types/redis_string.cc
+++ b/src/types/redis_string.cc
@@ -27,6 +27,7 @@
#include <string>
#include "parse_util.h"
+#include "server/redis_request.h"
#include "storage/redis_metadata.h"
#include "time_util.h"
@@ -530,4 +531,156 @@ rocksdb::Status String::CAD(const std::string &user_key,
const std::string &valu
return rocksdb::Status::OK();
}
+rocksdb::Status String::LCS(const std::string &user_key1, const std::string
&user_key2, StringLCSArgs args,
+ StringLCSResult *rst) {
+ if (args.type == StringLCSType::LEN) {
+ *rst = static_cast<uint32_t>(0);
+ } else if (args.type == StringLCSType::IDX) {
+ *rst = StringLCSIdxResult{{}, 0};
+ } else {
+ *rst = std::string{};
+ }
+
+ std::string a;
+ std::string b;
+ std::string ns_key1 = AppendNamespacePrefix(user_key1);
+ std::string ns_key2 = AppendNamespacePrefix(user_key2);
+ auto s1 = getValue(ns_key1, &a);
+ auto s2 = getValue(ns_key2, &b);
+
+ if (!s1.ok() && !s1.IsNotFound()) {
+ return s1;
+ }
+ if (!s2.ok() && !s2.IsNotFound()) {
+ return s2;
+ }
+ if (s1.IsNotFound()) a = "";
+ if (s2.IsNotFound()) b = "";
+
+ // Detect string truncation or later overflows.
+ if (a.length() >= UINT32_MAX - 1 || b.length() >= UINT32_MAX - 1) {
+ return rocksdb::Status::InvalidArgument("String too long for LCS");
+ }
+
+ // Compute the LCS using the vanilla dynamic programming technique of
+ // building a table of LCS(x, y) substrings.
+ auto alen = static_cast<uint32_t>(a.length());
+ auto blen = static_cast<uint32_t>(b.length());
+
+ // Allocate the LCS table.
+ uint64_t dp_size = (alen + 1) * (blen + 1);
+ uint64_t bulk_size = dp_size * sizeof(uint32_t);
+ if (bulk_size > PROTO_BULK_MAX_SIZE || bulk_size / dp_size !=
sizeof(uint32_t)) {
+ return rocksdb::Status::Aborted("Insufficient memory, transient memory for
LCS exceeds proto-max-bulk-len");
+ }
+ std::vector<uint32_t> dp(dp_size, 0);
+ auto lcs = [&dp, blen](const uint32_t i, const uint32_t j) -> uint32_t & {
return dp[i * (blen + 1) + j]; };
+
+ // Start building the LCS table.
+ for (uint32_t i = 1; i <= alen; i++) {
+ for (uint32_t j = 1; j <= blen; j++) {
+ if (a[i - 1] == b[j - 1]) {
+ // The len LCS (and the LCS itself) of two
+ // sequences with the same final character, is the
+ // LCS of the two sequences without the last char
+ // plus that last char.
+ lcs(i, j) = lcs(i - 1, j - 1) + 1;
+ } else {
+ // If the last character is different, take the longest
+ // between the LCS of the first string and the second
+ // minus the last char, and the reverse.
+ lcs(i, j) = std::max(lcs(i - 1, j), lcs(i, j - 1));
+ }
+ }
+ }
+
+ uint32_t idx = lcs(alen, blen);
+
+ // Only compute the length of LCS.
+ if (auto result = std::get_if<uint32_t>(rst)) {
+ *result = idx;
+ return rocksdb::Status::OK();
+ }
+
+ // Store the length of the LCS first if needed.
+ if (auto result = std::get_if<StringLCSIdxResult>(rst)) {
+ result->len = idx;
+ }
+
+ // Allocate when we need to compute the actual LCS string.
+ if (auto result = std::get_if<std::string>(rst)) {
+ result->resize(idx);
+ }
+
+ uint32_t i = alen;
+ uint32_t j = blen;
+ uint32_t arange_start = alen; // alen signals that values are not set.
+ uint32_t arange_end = 0;
+ uint32_t brange_start = 0;
+ uint32_t brange_end = 0;
+ while (i > 0 && j > 0) {
+ bool emit_range = false;
+ if (a[i - 1] == b[j - 1]) {
+ // If there is a match, store the character if needed.
+ // And reduce the indexes to look for a new match.
+ if (auto result = std::get_if<std::string>(rst)) {
+ result->at(idx - 1) = a[i - 1];
+ }
+
+ // Track the current range.
+ if (arange_start == alen) {
+ arange_start = i - 1;
+ arange_end = i - 1;
+ brange_start = j - 1;
+ brange_end = j - 1;
+ }
+ // Let's see if we can extend the range backward since
+ // it is contiguous.
+ else if (arange_start == i && brange_start == j) {
+ arange_start--;
+ brange_start--;
+ } else {
+ emit_range = true;
+ }
+
+ // Emit the range if we matched with the first byte of
+ // one of the two strings. We'll exit the loop ASAP.
+ if (arange_start == 0 || brange_start == 0) {
+ emit_range = true;
+ }
+ idx--;
+ i--;
+ j--;
+ } else {
+ // Otherwise reduce i and j depending on the largest
+ // LCS between, to understand what direction we need to go.
+ uint32_t lcs1 = lcs(i - 1, j);
+ uint32_t lcs2 = lcs(i, j - 1);
+ if (lcs1 > lcs2)
+ i--;
+ else
+ j--;
+ if (arange_start != alen) emit_range = true;
+ }
+
+ // Emit the current range if needed.
+ if (emit_range) {
+ if (auto result = std::get_if<StringLCSIdxResult>(rst)) {
+ uint32_t match_len = arange_end - arange_start + 1;
+
+ // Always emit the range when the `min_match_len` is not set.
+ if (args.min_match_len == 0 || match_len >= args.min_match_len) {
+ result->matches.emplace_back(StringLCSRange{arange_start,
arange_end},
+ StringLCSRange{brange_start,
brange_end}, match_len);
+ }
+ }
+
+ // Restart at the next match.
+ arange_start = alen;
+ }
+ }
+
+ return rocksdb::Status::OK();
+}
+
} // namespace redis
diff --git a/src/types/redis_string.h b/src/types/redis_string.h
index a6eddccf..166acc63 100644
--- a/src/types/redis_string.h
+++ b/src/types/redis_string.h
@@ -23,6 +23,7 @@
#include <cstdint>
#include <optional>
#include <string>
+#include <variant>
#include <vector>
#include "storage/redis_db.h"
@@ -42,8 +43,36 @@ struct StringSetArgs {
bool keep_ttl;
};
-namespace redis {
+enum class StringLCSType { NONE, LEN, IDX };
+
+struct StringLCSArgs {
+ StringLCSType type;
+ int64_t min_match_len;
+};
+
+struct StringLCSRange {
+ uint32_t start;
+ uint32_t end;
+};
+
+struct StringLCSMatchedRange {
+ StringLCSRange a;
+ StringLCSRange b;
+ uint32_t match_len;
+ StringLCSMatchedRange(StringLCSRange ra, StringLCSRange rb, uint32_t len) :
a(ra), b(rb), match_len(len) {}
+};
+
+struct StringLCSIdxResult {
+ // Matched ranges.
+ std::vector<StringLCSMatchedRange> matches;
+ // LCS length.
+ uint32_t len;
+};
+
+using StringLCSResult = std::variant<std::string, uint32_t,
StringLCSIdxResult>;
+
+namespace redis {
class String : public Database {
public:
explicit String(engine::Storage *storage, const std::string &ns) :
Database(storage, ns) {}
@@ -68,6 +97,8 @@ class String : public Database {
rocksdb::Status CAS(const std::string &user_key, const std::string
&old_value, const std::string &new_value,
uint64_t ttl, int *flag);
rocksdb::Status CAD(const std::string &user_key, const std::string &value,
int *flag);
+ rocksdb::Status LCS(const std::string &user_key1, const std::string
&user_key2, StringLCSArgs args,
+ StringLCSResult *rst);
private:
rocksdb::Status getValue(const std::string &ns_key, std::string *value);
diff --git a/tests/cppunit/types/string_test.cc
b/tests/cppunit/types/string_test.cc
index 74a3d2da..1c850a71 100644
--- a/tests/cppunit/types/string_test.cc
+++ b/tests/cppunit/types/string_test.cc
@@ -307,3 +307,54 @@ TEST_F(RedisStringTest, CAD) {
status = string_->Del(key);
}
+
+TEST_F(RedisStringTest, LCS) {
+ auto expect_result_eq = [](const StringLCSIdxResult &val1, const
StringLCSIdxResult &val2) {
+ ASSERT_EQ(val1.len, val2.len);
+ ASSERT_EQ(val1.matches.size(), val2.matches.size());
+ for (size_t i = 0; i < val1.matches.size(); i++) {
+ ASSERT_EQ(val1.matches[i].match_len, val2.matches[i].match_len);
+ ASSERT_EQ(val1.matches[i].a.start, val2.matches[i].a.start);
+ ASSERT_EQ(val1.matches[i].a.end, val2.matches[i].a.end);
+ ASSERT_EQ(val1.matches[i].b.start, val2.matches[i].b.start);
+ ASSERT_EQ(val1.matches[i].b.end, val2.matches[i].b.end);
+ }
+ };
+
+ StringLCSResult rst;
+ std::string key1 = "lcs_key1";
+ std::string key2 = "lcs_key2";
+ std::string value1 = "abcdef";
+ std::string value2 = "acdf";
+
+ auto status = string_->Set(key1, value1);
+ ASSERT_TRUE(status.ok());
+ status = string_->Set(key2, value2);
+ ASSERT_TRUE(status.ok());
+
+ status = string_->LCS(key1, key2, {}, &rst);
+ ASSERT_TRUE(status.ok());
+ EXPECT_EQ("acdf", std::get<std::string>(rst));
+
+ status = string_->LCS(key1, key2, {StringLCSType::LEN}, &rst);
+ ASSERT_TRUE(status.ok());
+ EXPECT_EQ(4, std::get<uint32_t>(rst));
+
+ status = string_->LCS(key1, key2, {StringLCSType::IDX}, &rst);
+ ASSERT_TRUE(status.ok());
+ expect_result_eq({{
+ {{5, 5}, {3, 3}, 1},
+ {{2, 3}, {1, 2}, 2},
+ {{0, 0}, {0, 0}, 1},
+ },
+ 4},
+ std::get<StringLCSIdxResult>(rst));
+
+ status = string_->LCS(key1, key2, {StringLCSType::IDX, 2}, &rst);
+ ASSERT_TRUE(status.ok());
+ expect_result_eq({{
+ {{2, 3}, {1, 2}, 2},
+ },
+ 4},
+ std::get<StringLCSIdxResult>(rst));
+}
diff --git a/tests/gocase/unit/type/strings/strings_test.go
b/tests/gocase/unit/type/strings/strings_test.go
index e3474b05..49963d0b 100644
--- a/tests/gocase/unit/type/strings/strings_test.go
+++ b/tests/gocase/unit/type/strings/strings_test.go
@@ -895,4 +895,108 @@ func TestString(t *testing.T) {
require.ErrorContains(t, rdb.Do(ctx, "CAD", "cad_key").Err(),
"ERR wrong number of arguments")
require.ErrorContains(t, rdb.Do(ctx, "CAD", "cad_key", "123",
"234").Err(), "ERR wrong number of arguments")
})
+
+ rna1 :=
"CACCTTCCCAGGTAACAAACCAACCAACTTTCGATCTCTTGTAGATCTGTTCTCTAAACGAACTTTAAAATCTGTGTGGCTGTCACTCGGCTGCATGCTTAGTGCACTCACGCAGTATAATTAATAACTAATTACTGTCGTTGACAGGACACGAGTAACTCGTCTATCTTCTGCAGGCTGCTTACGGTTTCGTCCGTGTTGCAGCCGATCATCAGCACATCTAGGTTTCGTCCGGGTGTG"
+ rna2 :=
"ATTAAAGGTTTATACCTTCCCAGGTAACAAACCAACCAACTTTCGATCTCTTGTAGATCTGTTCTCTAAACGAACTTTAAAATCTGTGTGGCTGTCACTCGGCTGCATGCTTAGTGCACTCACGCAGTATAATTAATAACTAATTACTGTCGTTGACAGGACACGAGTAACTCGTCTATCTTCTGCAGGCTGCTTACGGTTTCGTCCGTGTTGCAGCCGATCATCAGCACATCTAGGTTT"
+ rnalcs :=
"ACCTTCCCAGGTAACAAACCAACCAACTTTCGATCTCTTGTAGATCTGTTCTCTAAACGAACTTTAAAATCTGTGTGGCTGTCACTCGGCTGCATGCTTAGTGCACTCACGCAGTATAATTAATAACTAATTACTGTCGTTGACAGGACACGAGTAACTCGTCTATCTTCTGCAGGCTGCTTACGGTTTCGTCCGTGTTGCAGCCGATCATCAGCACATCTAGGTTT"
+
+ t.Run("LCS basic", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", rna2, 0).Err())
+ require.Equal(t, rnalcs, rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus1", Key2: "virus2"}).Val().MatchString)
+ })
+
+ t.Run("LCS len", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", rna2, 0).Err())
+ require.Equal(t, int64(len(rnalcs)), rdb.LCS(ctx,
&redis.LCSQuery{Key1: "virus1", Key2: "virus2", Len: true}).Val().Len)
+ })
+
+ t.Run("LCS indexes", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", rna2, 0).Err())
+ matches := rdb.LCS(ctx, &redis.LCSQuery{Key1: "virus1", Key2:
"virus2", Idx: true}).Val().Matches
+ require.Equal(t, []redis.LCSMatchedPosition{
+ {
+ Key1: redis.LCSPosition{Start: 238, End: 238},
+ Key2: redis.LCSPosition{Start: 239, End: 239},
+ },
+ {
+ Key1: redis.LCSPosition{Start: 236, End: 236},
+ Key2: redis.LCSPosition{Start: 238, End: 238},
+ },
+ {
+ Key1: redis.LCSPosition{Start: 229, End: 230},
+ Key2: redis.LCSPosition{Start: 236, End: 237},
+ },
+ {
+ Key1: redis.LCSPosition{Start: 224, End: 224},
+ Key2: redis.LCSPosition{Start: 235, End: 235},
+ },
+ {
+ Key1: redis.LCSPosition{Start: 1, End: 222},
+ Key2: redis.LCSPosition{Start: 13, End: 234},
+ },
+ }, matches)
+ })
+
+ t.Run("LCS indexes with match len", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", rna2, 0).Err())
+ matches := rdb.LCS(ctx, &redis.LCSQuery{Key1: "virus1", Key2:
"virus2", Idx: true, WithMatchLen: true}).Val().Matches
+ require.Equal(t, []redis.LCSMatchedPosition{
+ {
+ Key1: redis.LCSPosition{Start: 238, End:
238},
+ Key2: redis.LCSPosition{Start: 239, End:
239},
+ MatchLen: 1,
+ },
+ {
+ Key1: redis.LCSPosition{Start: 236, End:
236},
+ Key2: redis.LCSPosition{Start: 238, End:
238},
+ MatchLen: 1,
+ },
+ {
+ Key1: redis.LCSPosition{Start: 229, End:
230},
+ Key2: redis.LCSPosition{Start: 236, End:
237},
+ MatchLen: 2,
+ },
+ {
+ Key1: redis.LCSPosition{Start: 224, End:
224},
+ Key2: redis.LCSPosition{Start: 235, End:
235},
+ MatchLen: 1,
+ },
+ {
+ Key1: redis.LCSPosition{Start: 1, End: 222},
+ Key2: redis.LCSPosition{Start: 13, End:
234},
+ MatchLen: 222,
+ },
+ }, matches)
+ })
+
+ t.Run("LCS indexes with match len and minimum match len", func(t
*testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", rna2, 0).Err())
+ matches := rdb.LCS(ctx, &redis.LCSQuery{Key1: "virus1", Key2:
"virus2", Idx: true, WithMatchLen: true, MinMatchLen: 5}).Val().Matches
+ require.Equal(t, []redis.LCSMatchedPosition{
+ {
+ Key1: redis.LCSPosition{Start: 1, End: 222},
+ Key2: redis.LCSPosition{Start: 13, End:
234},
+ MatchLen: 222,
+ },
+ }, matches)
+ })
+
+ t.Run("LCS empty", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "virus1", rna1, 0).Err())
+ require.NoError(t, rdb.Set(ctx, "virus2", "", 0).Err())
+
+ require.Equal(t, rna1, rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus1", Key2: "virus1"}).Val().MatchString)
+ require.Equal(t, "", rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus1", Key2: "virus2"}).Val().MatchString)
+ require.Equal(t, "", rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus2", Key2: "virus1"}).Val().MatchString)
+ require.Equal(t, "", rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus2", Key2: "virus2"}).Val().MatchString)
+
+ require.Equal(t, int64(0), rdb.LCS(ctx, &redis.LCSQuery{Key1:
"virus1", Key2: "virus2"}).Val().Len)
+ require.Equal(t, []redis.LCSMatchedPosition{}, rdb.LCS(ctx,
&redis.LCSQuery{Key1: "virus1", Key2: "virus2", Idx: true}).Val().Matches)
+ require.Equal(t, []redis.LCSMatchedPosition{}, rdb.LCS(ctx,
&redis.LCSQuery{Key1: "virus1", Key2: "virus2", Idx: true, WithMatchLen:
true}).Val().Matches)
+ })
}