From e47bd84293c1f9c813f40e14fe496b56671505a6 Mon Sep 17 00:00:00 2001 From: Ankush Chavan Date: Fri, 15 May 2026 19:37:56 +0530 Subject: [PATCH] feat(cmd): implement TTL and PTTL commands Signed-off-by: Ankush Chavan --- README.md | 2 + cmd/ttl.go | 52 ++++++++++++++++++++++++ cmd/ttl_test.go | 89 +++++++++++++++++++++++++++++++++++++++++ common/constants.go | 2 + docs/README.md | 2 + docs/pttl.md | 40 ++++++++++++++++++ docs/ttl.md | 40 ++++++++++++++++++ exc/command_executor.go | 4 ++ 8 files changed, 231 insertions(+) create mode 100644 cmd/ttl.go create mode 100644 cmd/ttl_test.go create mode 100644 docs/pttl.md create mode 100644 docs/ttl.md diff --git a/README.md b/README.md index 1331ca8..a52e3b1 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,5 @@ See the [Usage Guide](./docs/usage.md) for installation, connecting, and a walkt | [CONFIG](./docs/config.md) | Read and modify server configuration | | [TYPE](./docs/type.md) | Return the type of the value stored at a key | | [EXISTS](./docs/exists.md) | Check if one or more keys exist | +| [TTL](./docs/ttl.md) | Get remaining TTL of a key in seconds | +| [PTTL](./docs/pttl.md) | Get remaining TTL of a key in milliseconds | diff --git a/cmd/ttl.go b/cmd/ttl.go new file mode 100644 index 0000000..0cd5c0d --- /dev/null +++ b/cmd/ttl.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "HelixDB/common" + "HelixDB/db" + "time" +) + +// TTL returns the remaining time-to-live of a key in seconds. +// Returns -1 if the key exists but has no TTL. +// Returns -2 if the key does not exist or has expired. +func TTL(command common.Cmd) ([]byte, error) { + if len(command.Args) != 1 { + err := common.WrongNumberOfArgsError(command.Name) + return common.RespError(err.Error()), err + } + return ttlMillis(command.Args[0], false), nil +} + +// PTTL returns the remaining time-to-live of a key in milliseconds. +// Returns -1 if the key exists but has no TTL. +// Returns -2 if the key does not exist or has expired. +func PTTL(command common.Cmd) ([]byte, error) { + if len(command.Args) != 1 { + err := common.WrongNumberOfArgsError(command.Name) + return common.RespError(err.Error()), err + } + return ttlMillis(command.Args[0], true), nil +} + +// ttlMillis looks up the TTL for key and returns it as a RESP integer. +// If millis is true the value is in milliseconds, otherwise seconds. +func ttlMillis(key string, millis bool) []byte { + expiry, ok := db.KeyTTL.Load(key) + if !ok { + // Key does not exist at all. + return common.RespInteger(-2) + } + if expiry == nil { + // Key exists with no TTL. + return common.RespInteger(-1) + } + remaining := expiry.(int64) - time.Now().UnixMilli() + if remaining <= 0 { + // Key has expired (passive check). + return common.RespInteger(-2) + } + if millis { + return common.RespInteger(int(remaining)) + } + return common.RespInteger(int(remaining / 1000)) +} diff --git a/cmd/ttl_test.go b/cmd/ttl_test.go new file mode 100644 index 0000000..befc9e6 --- /dev/null +++ b/cmd/ttl_test.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "HelixDB/common" + "HelixDB/db" + "errors" + "reflect" + "testing" +) + +func TestTTL(t *testing.T) { + // Key with no TTL + _, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_persist", "v"}}) + // Key with TTL (~100 seconds from now) + _, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_expiring", "v", "EX", "100"}}) + // Expired key (TTL set to the past) + db.DB.Store("ttl_expired", db.NewValue("v")) + db.KeyTTL.Store("ttl_expired", int64(1)) // epoch ms well in the past + + tests := []struct { + command common.Cmd + want []byte + wantErr error + }{ + // Key with no TTL returns -1 + {common.Cmd{Name: "TTL", Args: []string{"ttl_persist"}}, common.RespInteger(-1), nil}, + // Non-existent key returns -2 + {common.Cmd{Name: "TTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil}, + // Expired key returns -2 + {common.Cmd{Name: "TTL", Args: []string{"ttl_expired"}}, common.RespInteger(-2), nil}, + // Wrong number of arguments + {common.Cmd{Name: "TTL"}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs}, + {common.Cmd{Name: "TTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs}, + } + for _, test := range tests { + if got, gotErr := TTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) { + t.Errorf("TTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr) + } + } + + // Key with TTL returns a positive integer (exact value is timing-sensitive) + got, gotErr := TTL(common.Cmd{Name: "TTL", Args: []string{"ttl_expiring"}}) + if gotErr != nil { + t.Errorf("TTL(ttl_expiring) unexpected error: %v", gotErr) + } + if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) { + t.Errorf("TTL(ttl_expiring) = %v; want a positive integer", got) + } +} + +func TestPTTL(t *testing.T) { + // Key with no TTL + _, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_persist", "v"}}) + // Key with TTL (~100 seconds from now) + _, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_expiring", "v", "EX", "100"}}) + // Expired key + db.DB.Store("pttl_expired", db.NewValue("v")) + db.KeyTTL.Store("pttl_expired", int64(1)) + + tests := []struct { + command common.Cmd + want []byte + wantErr error + }{ + // Key with no TTL returns -1 + {common.Cmd{Name: "PTTL", Args: []string{"pttl_persist"}}, common.RespInteger(-1), nil}, + // Non-existent key returns -2 + {common.Cmd{Name: "PTTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil}, + // Expired key returns -2 + {common.Cmd{Name: "PTTL", Args: []string{"pttl_expired"}}, common.RespInteger(-2), nil}, + // Wrong number of arguments + {common.Cmd{Name: "PTTL"}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs}, + {common.Cmd{Name: "PTTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs}, + } + for _, test := range tests { + if got, gotErr := PTTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) { + t.Errorf("PTTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr) + } + } + + // Key with TTL returns a positive integer + got, gotErr := PTTL(common.Cmd{Name: "PTTL", Args: []string{"pttl_expiring"}}) + if gotErr != nil { + t.Errorf("PTTL(pttl_expiring) unexpected error: %v", gotErr) + } + if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) { + t.Errorf("PTTL(pttl_expiring) = %v; want a positive integer", got) + } +} diff --git a/common/constants.go b/common/constants.go index 18479f5..5fea57d 100644 --- a/common/constants.go +++ b/common/constants.go @@ -15,6 +15,8 @@ const Keys = "KEYS" const Config = "CONFIG" const Type = "TYPE" const Exists = "EXISTS" +const TTL = "TTL" +const PTTL = "PTTL" // Command Args // Expiration Args diff --git a/docs/README.md b/docs/README.md index dcf41bc..8799b1e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,3 +18,5 @@ | [CONFIG](./config.md) | Read and modify server configuration | | [TYPE](./type.md) | Return the type of the value stored at a key | | [EXISTS](./exists.md) | Check if one or more keys exist | +| [TTL](./ttl.md) | Get remaining TTL of a key in seconds | +| [PTTL](./pttl.md) | Get remaining TTL of a key in milliseconds | diff --git a/docs/pttl.md b/docs/pttl.md new file mode 100644 index 0000000..6ec924c --- /dev/null +++ b/docs/pttl.md @@ -0,0 +1,40 @@ +# PTTL + +Returns the remaining time-to-live of a key in milliseconds. + +## Syntax + +``` +PTTL key +``` + +## Return value + +| Value | Meaning | +|-------|------------------------------------------| +| `N` | Remaining TTL in milliseconds (N ≥ 1) | +| `-1` | Key exists but has no TTL | +| `-2` | Key does not exist or has expired | + +## Errors + +- `wrong number of arguments for 'pttl' command` — incorrect number of arguments. + +## Examples + +``` +> SET counter 42 EX 100 +OK + +> PTTL counter +99743 + +> SET greeting hello +OK + +> PTTL greeting +-1 + +> PTTL missing +-2 +``` diff --git a/docs/ttl.md b/docs/ttl.md new file mode 100644 index 0000000..406fa34 --- /dev/null +++ b/docs/ttl.md @@ -0,0 +1,40 @@ +# TTL + +Returns the remaining time-to-live of a key in seconds. + +## Syntax + +``` +TTL key +``` + +## Return value + +| Value | Meaning | +|-------|-------------------------------------| +| `N` | Remaining TTL in seconds (N ≥ 1) | +| `-1` | Key exists but has no TTL | +| `-2` | Key does not exist or has expired | + +## Errors + +- `wrong number of arguments for 'ttl' command` — incorrect number of arguments. + +## Examples + +``` +> SET counter 42 EX 100 +OK + +> TTL counter +99 + +> SET greeting hello +OK + +> TTL greeting +-1 + +> TTL missing +-2 +``` diff --git a/exc/command_executor.go b/exc/command_executor.go index 347437a..3178014 100644 --- a/exc/command_executor.go +++ b/exc/command_executor.go @@ -32,6 +32,10 @@ func ExecuteCommand(command common.Cmd) ([]byte, error) { return cmd.Type(command) case common.Exists: return cmd.Exists(command) + case common.TTL: + return cmd.TTL(command) + case common.PTTL: + return cmd.PTTL(command) } return []byte("-unsupported command\r\n"), UnsupportedCommand }