Repository: redis/rueidis

Issue:
Add `TotalNetIn` and other new fields to `rueidiscompat.ClientInfo`
https://github.com/redis/redis/pull/13944 CLIENT INFO and CLIENT LIST have the following new fields:
1. tot-net-in: total network bytes read from the client connection
2. tot-net-out: total network bytes sent to the client connection
3. tot-cmds: number of commands executed by the client connection

We should add `TotalNetIn`, `TotalNetOut`, and `TotalCmds` to `rueidiscompat.ClientInfo` and fill the corresponding values.

See: https://github.com/redis/go-redis/pull/3409/files#diff-4d7d1923693fc5ce892add2ea2907a744e77ea0b50c1939ccc5067cb48a466a3R5200-R5202.

Hints from issue comments:
@Aakkash-Suresh: @rueian I'd like to take a shot at this

Solution patch:
diff --git a/hack/cmds/commands_vector_sets.json b/hack/cmds/commands_vector_sets.json
index 7b9f790..65576a8 100644
--- a/hack/cmds/commands_vector_sets.json
+++ b/hack/cmds/commands_vector_sets.json
@@ -313,6 +313,11 @@
         "type": "command",
         "optional": true
       },
+      {
+        "command": "WITHATTRIBS",
+        "type": "command",
+        "optional": true
+      },
       {
         "command": "COUNT",
         "name": "num",
diff --git a/internal/cmds/gen_vector_set.go b/internal/cmds/gen_vector_set.go
index 681ab7f..f0f79e9 100644
--- a/internal/cmds/gen_vector_set.go
+++ b/internal/cmds/gen_vector_set.go
@@ -726,6 +726,11 @@ func (c VsimQueryTypeEleElement) Withscores() VsimWithscores {
 	return (VsimWithscores)(c)
 }
 
+func (c VsimQueryTypeEleElement) Withattribs() VsimWithattribs {
+	c.cs.s = append(c.cs.s, "WITHATTRIBS")
+	return (VsimWithattribs)(c)
+}
+
 func (c VsimQueryTypeEleElement) Count(num int64) VsimCount {
 	c.cs.s = append(c.cs.s, "COUNT", strconv.FormatInt(num, 10))
 	return (VsimCount)(c)
@@ -775,6 +780,11 @@ func (c VsimQueryTypeFp32Vector) Withscores() VsimWithscores {
 	return (VsimWithscores)(c)
 }
 
+func (c VsimQueryTypeFp32Vector) Withattribs() VsimWithattribs {
+	c.cs.s = append(c.cs.s, "WITHATTRIBS")
+	return (VsimWithattribs)(c)
+}
+
 func (c VsimQueryTypeFp32Vector) Count(num int64) VsimCount {
 	c.cs.s = append(c.cs.s, "COUNT", strconv.FormatInt(num, 10))
 	return (VsimCount)(c)
@@ -833,6 +843,11 @@ func (c VsimQueryTypeValuesVector) Withscores() VsimWithscores {
 	return (VsimWithscores)(c)
 }
 
+func (c VsimQueryTypeValuesVector) Withattribs() VsimWithattribs {
+	c.cs.s = append(c.cs.s, "WITHATTRIBS")
+	return (VsimWithattribs)(c)
+}
+
 func (c VsimQueryTypeValuesVector) Count(num int64) VsimCount {
 	c.cs.s = append(c.cs.s, "COUNT", strconv.FormatInt(num, 10))
 	return (VsimCount)(c)
@@ -880,8 +895,50 @@ func (c VsimTruth) Build() Completed {
 	return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks}
 }
 
+type VsimWithattribs Incomplete
+
+func (c VsimWithattribs) Count(num int64) VsimCount {
+	c.cs.s = append(c.cs.s, "COUNT", strconv.FormatInt(num, 10))
+	return (VsimCount)(c)
+}
+
+func (c VsimWithattribs) Ef(searchExplorationFactor int64) VsimEf {
+	c.cs.s = append(c.cs.s, "EF", strconv.FormatInt(searchExplorationFactor, 10))
+	return (VsimEf)(c)
+}
+
+func (c VsimWithattribs) Filter(expression string) VsimFilter {
+	c.cs.s = append(c.cs.s, "FILTER", expression)
+	return (VsimFilter)(c)
+}
+
+func (c VsimWithattribs) FilterEf(maxFilteringEffort int64) VsimFilterEf {
+	c.cs.s = append(c.cs.s, "FILTER-EF", strconv.FormatInt(maxFilteringEffort, 10))
+	return (VsimFilterEf)(c)
+}
+
+func (c VsimWithattribs) Truth() VsimTruth {
+	c.cs.s = append(c.cs.s, "TRUTH")
+	return (VsimTruth)(c)
+}
+
+func (c VsimWithattribs) Nothread() VsimNothread {
+	c.cs.s = append(c.cs.s, "NOTHREAD")
+	return (VsimNothread)(c)
+}
+
+func (c VsimWithattribs) Build() Completed {
+	c.cs.Build()
+	return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks}
+}
+
 type VsimWithscores Incomplete
 
+func (c VsimWithscores) Withattribs() VsimWithattribs {
+	c.cs.s = append(c.cs.s, "WITHATTRIBS")
+	return (VsimWithattribs)(c)
+}
+
 func (c VsimWithscores) Count(num int64) VsimCount {
 	c.cs.s = append(c.cs.s, "COUNT", strconv.FormatInt(num, 10))
 	return (VsimCount)(c)
diff --git a/rueidiscompat/command.go b/rueidiscompat/command.go
index 73864ae..163f3ef 100644
--- a/rueidiscompat/command.go
+++ b/rueidiscompat/command.go
@@ -4711,6 +4711,9 @@ type ClientInfo struct {
 	TotalMemory        int           // tot-mem, total memory consumed by this client in its various buffers
 	Redir              int64         // client id of current client tracking redirection
 	Resp               int           // redis version 7.0, client RESP protocol version
+	TotalNetIn         int64         // tot-net-in, total network bytes read from the client connection
+	TotalNetOut        int64         // tot-net-out, total network bytes sent to the client connection
+	TotalCmds          int64         // tot-cmds, number of commands executed by the client connection
 }
 
 type ClientInfoCmd struct {
@@ -4857,6 +4860,12 @@ func stringToClientInfo(txt string) (*ClientInfo, error) {
 			info.LibName = val
 		case "lib-ver":
 			info.LibVer = val
+		case "tot-net-in":
+			info.TotalNetIn, err = strconv.ParseInt(val, 10, 64)
+		case "tot-net-out":
+			info.TotalNetOut, err = strconv.ParseInt(val, 10, 64)
+		case "tot-cmds":
+			info.TotalCmds, err = strconv.ParseInt(val, 10, 64)
 		}
 
 		if err != nil {



Test patch:
diff --git a/internal/cmds/gen_vector_set_test.go b/internal/cmds/gen_vector_set_test.go
index be3da24..c27d9b4 100644
--- a/internal/cmds/gen_vector_set_test.go
+++ b/internal/cmds/gen_vector_set_test.go
@@ -46,21 +46,28 @@ func vector_set0(s Builder) {
 	s.Vrandmember().Key("1").Build()
 	s.Vrem().Key("1").Element("1").Build()
 	s.Vsetattr().Key("1").Element("1").Json("1").Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").FilterEf(1).Truth().Nothread().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").FilterEf(1).Truth().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").FilterEf(1).Nothread().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").FilterEf(1).Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").Truth().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").Nothread().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Filter("1").Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).FilterEf(1).Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Truth().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Nothread().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Ef(1).Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Filter("1").Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).FilterEf(1).Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Truth().Build()
-	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").FilterEf(1).Truth().Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").FilterEf(1).Truth().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").FilterEf(1).Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").FilterEf(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").Truth().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Filter("1").Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).FilterEf(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Truth().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Ef(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Filter("1").Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).FilterEf(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Truth().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Count(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Ef(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Filter("1").Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().FilterEf(1).Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Truth().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Nothread().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withscores().Withattribs().Build()
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Count(1).Build()
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Ef(1).Build()
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Filter("1").Build()
@@ -68,6 +75,7 @@ func vector_set0(s Builder) {
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Truth().Build()
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Nothread().Build()
 	s.Vsim().Key("1").Ele().Element("1").Withscores().Build()
+	s.Vsim().Key("1").Ele().Element("1").Withattribs().Build()
 	s.Vsim().Key("1").Ele().Element("1").Count(1).Build()
 	s.Vsim().Key("1").Ele().Element("1").Ef(1).Build()
 	s.Vsim().Key("1").Ele().Element("1").Filter("1").Build()
@@ -76,6 +84,7 @@ func vector_set0(s Builder) {
 	s.Vsim().Key("1").Ele().Element("1").Nothread().Build()
 	s.Vsim().Key("1").Ele().Element("1").Build()
 	s.Vsim().Key("1").Fp32().Vector("1").Withscores().Build()
+	s.Vsim().Key("1").Fp32().Vector("1").Withattribs().Build()
 	s.Vsim().Key("1").Fp32().Vector("1").Count(1).Build()
 	s.Vsim().Key("1").Fp32().Vector("1").Ef(1).Build()
 	s.Vsim().Key("1").Fp32().Vector("1").Filter("1").Build()
@@ -84,6 +93,7 @@ func vector_set0(s Builder) {
 	s.Vsim().Key("1").Fp32().Vector("1").Nothread().Build()
 	s.Vsim().Key("1").Fp32().Vector("1").Build()
 	s.Vsim().Key("1").Values(1).Vector().Vector().Withscores().Build()
+	s.Vsim().Key("1").Values(1).Vector().Vector().Withattribs().Build()
 	s.Vsim().Key("1").Values(1).Vector().Vector().Count(1).Build()
 	s.Vsim().Key("1").Values(1).Vector().Vector().Ef(1).Build()
 	s.Vsim().Key("1").Values(1).Vector().Vector().Filter("1").Build()
diff --git a/rueidiscompat/command_test.go b/rueidiscompat/command_test.go
index d1a99eb..0ba0400 100644
--- a/rueidiscompat/command_test.go
+++ b/rueidiscompat/command_test.go
@@ -1151,3 +1151,35 @@ func TestCommandErrorHandling(t *testing.T) {
 		})
 	}
 }
+
+func TestClientInfoParsing(t *testing.T) {
+	// Test case with the new fields
+	testData := "id=123 addr=127.0.0.1:6379 fd=5 name= age=10 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=ping user=default redir=-1 resp=3 lib-name=redis-go lib-ver=8.0.0 tot-net-in=1024 tot-net-out=2048 tot-cmds=100"
+
+	info, err := stringToClientInfo(testData)
+	if err != nil {
+		t.Fatalf("Failed to parse client info: %v", err)
+	}
+
+	// Verify the new fields are parsed correctly
+	if info.TotalNetIn != 1024 {
+		t.Errorf("Expected TotalNetIn to be 1024, got %d", info.TotalNetIn)
+	}
+
+	if info.TotalNetOut != 2048 {
+		t.Errorf("Expected TotalNetOut to be 2048, got %d", info.TotalNetOut)
+	}
+
+	if info.TotalCmds != 100 {
+		t.Errorf("Expected TotalCmds to be 100, got %d", info.TotalCmds)
+	}
+
+	// Verify other fields are still parsed correctly
+	if info.ID != 123 {
+		t.Errorf("Expected ID to be 123, got %d", info.ID)
+	}
+
+	if info.Addr != "127.0.0.1:6379" {
+		t.Errorf("Expected Addr to be '127.0.0.1:6379', got '%s'", info.Addr)
+	}
+}