Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ See the [Usage Guide](./docs/usage.md) for installation, connecting, and a walkt
| [EXPIRE](./docs/expire.md) | Set a TTL on an existing key (in seconds) |
| [KEYS](./docs/keys.md) | Find keys matching a pattern |
| [CONFIG](./docs/config.md) | Read and modify server configuration |
| [TYPE](./docs/type.md) | Return the type of the value stored at a key |
2 changes: 1 addition & 1 deletion cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ func GetValueFromMemory(key string) (string, error) {
if !ok {
return "", fmt.Errorf("key not found")
}
return data.(string), nil
return data.(db.Value).Raw, nil
}
4 changes: 2 additions & 2 deletions cmd/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func processSetArgs(key string, args map[string]any) error {
return nil
}

// setValueInMemory stores the value in the in-memory DB against the key.
// setValueInMemory detects the type of value and stores it in the in-memory DB.
func setValueInMemory(key string, value string) {
db.DB.Store(key, value)
db.DB.Store(key, db.NewValue(value))
}
34 changes: 34 additions & 0 deletions cmd/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cmd

import (
"HelixDB/common"
"HelixDB/db"
"time"
)

// Type returns the type of the value stored at key.
// Returns "none" if the key does not exist or has expired.
// Possible return values: string, integer, float, boolean, json, none.
func Type(command common.Cmd) ([]byte, error) {
if len(command.Args) != 1 {
err := common.WrongNumberOfArgsError(command.Name)
return common.RespError(err.Error()), err
}

key := command.Args[0]

expirationTime, ok := db.KeyTTL.Load(key)
if !ok {
return common.RespSimpleString("none"), nil
}
if expirationTime != nil && expirationTime.(int64) < time.Now().UnixMilli() {
return common.RespSimpleString("none"), nil
}

data, ok := db.DB.Load(key)
if !ok {
return common.RespSimpleString("none"), nil
}

return common.RespSimpleString(string(data.(db.Value).Type)), nil
}
51 changes: 51 additions & 0 deletions cmd/type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"HelixDB/common"
"HelixDB/db"
"errors"
"reflect"
"testing"
)

// TestType tests the Type command for all possible inputs.
func TestType(t *testing.T) {
// Seed keys of each type
db.DB.Store("type_string", db.NewValue("hello"))
db.KeyTTL.Store("type_string", nil)

db.DB.Store("type_integer", db.NewValue("42"))
db.KeyTTL.Store("type_integer", nil)

db.DB.Store("type_float", db.NewValue("3.14"))
db.KeyTTL.Store("type_float", nil)

db.DB.Store("type_boolean", db.NewValue("true"))
db.KeyTTL.Store("type_boolean", nil)

db.DB.Store("type_json", db.NewValue(`{"a":1}`))
db.KeyTTL.Store("type_json", nil)

tests := []struct {
command common.Cmd
want []byte
wantErr error
}{
{common.Cmd{Name: "TYPE", Args: []string{"type_string"}}, common.RespSimpleString("string"), nil},
{common.Cmd{Name: "TYPE", Args: []string{"type_integer"}}, common.RespSimpleString("integer"), nil},
{common.Cmd{Name: "TYPE", Args: []string{"type_float"}}, common.RespSimpleString("float"), nil},
{common.Cmd{Name: "TYPE", Args: []string{"type_boolean"}}, common.RespSimpleString("boolean"), nil},
{common.Cmd{Name: "TYPE", Args: []string{"type_json"}}, common.RespSimpleString("json"), nil},
// Non-existent key
{common.Cmd{Name: "TYPE", Args: []string{"no_such_key"}}, common.RespSimpleString("none"), nil},
// Wrong number of arguments
{common.Cmd{Name: "TYPE"}, common.RespError("wrong number of arguments for 'type' command"), common.ErrWrongNumberOfArgs},
{common.Cmd{Name: "TYPE", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'type' command"), common.ErrWrongNumberOfArgs},
}
for _, test := range tests {
got, gotErr := Type(test.command)
if !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) {
t.Errorf("Type(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr)
}
}
}
1 change: 1 addition & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Expire = "EXPIRE"
const Del = "DEL"
const Keys = "KEYS"
const Config = "CONFIG"
const Type = "TYPE"

// Command Args
// Expiration Args
Expand Down
5 changes: 5 additions & 0 deletions common/resp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func RespBulkStringArray(items []string) []byte {
return buffer.Bytes()
}

// RespSimpleString formats a value as a RESP simple string: +value\r\n
func RespSimpleString(value string) []byte {
return []byte("+" + value + Terminator)
}

func RespInteger(n int) []byte {
var buffer bytes.Buffer
buffer.WriteString(":")
Expand Down
44 changes: 44 additions & 0 deletions db/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package db

import (
"encoding/json"
"strconv"
)

// ValueType represents the detected type of a stored value.
type ValueType string

const (
TypeString ValueType = "string"
TypeInteger ValueType = "integer"
TypeFloat ValueType = "float"
TypeBoolean ValueType = "boolean"
TypeJSON ValueType = "json"
)

// Value wraps a stored string with its detected type.
// Raw always holds the original string representation so GET can return it
// unchanged, regardless of the detected type.
type Value struct {
Type ValueType
Raw string
}

// NewValue detects the type of raw and returns a Value.
// Detection order: integer → float → boolean → JSON → string.
func NewValue(raw string) Value {
if _, err := strconv.ParseInt(raw, 10, 64); err == nil {
return Value{Type: TypeInteger, Raw: raw}
}
if _, err := strconv.ParseFloat(raw, 64); err == nil {
return Value{Type: TypeFloat, Raw: raw}
}
if _, err := strconv.ParseBool(raw); err == nil {
return Value{Type: TypeBoolean, Raw: raw}
}
var js json.RawMessage
if err := json.Unmarshal([]byte(raw), &js); err == nil {
return Value{Type: TypeJSON, Raw: raw}
}
return Value{Type: TypeString, Raw: raw}
}
40 changes: 40 additions & 0 deletions db/value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package db

import (
"testing"
)

// TestNewValue tests that NewValue correctly detects the type of a raw string.
func TestNewValue(t *testing.T) {
tests := []struct {
raw string
wantType ValueType
}{
// Integers
{"42", TypeInteger},
{"-1", TypeInteger},
{"0", TypeInteger},
// Floats
{"3.14", TypeFloat},
{"-0.5", TypeFloat},
// Booleans
{"true", TypeBoolean},
{"false", TypeBoolean},
// JSON objects and arrays
{`{"key":"value"}`, TypeJSON},
{`[1,2,3]`, TypeJSON},
// Plain strings
{"hello", TypeString},
{"", TypeString},
{"hello world", TypeString},
}
for _, test := range tests {
got := NewValue(test.raw)
if got.Type != test.wantType {
t.Errorf("NewValue(%q).Type = %q; want %q", test.raw, got.Type, test.wantType)
}
if got.Raw != test.raw {
t.Errorf("NewValue(%q).Raw = %q; want %q", test.raw, got.Raw, test.raw)
}
}
}
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
| [EXPIRE](./expire.md) | Set a TTL on an existing key (in seconds) |
| [KEYS](./keys.md) | Find keys matching a pattern |
| [CONFIG](./config.md) | Read and modify server configuration |
| [TYPE](./type.md) | Return the type of the value stored at a key |
65 changes: 65 additions & 0 deletions docs/type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# TYPE

Returns the type of the value stored at a key.

> **Note:** HelixDB's `TYPE` command returns the *value type* (the detected type of the stored string), not the *data structure type* as Redis does. In Redis, `TYPE` always returns `string` for any string key. HelixDB extends this to distinguish integers, floats, booleans, and JSON. When data structure types (List, Hash, Set, Sorted Set) are added in future, `TYPE` will return their names as Redis does.

## Syntax

```
TYPE key
```

## Return value

One of the following strings:

| Value | Meaning |
|-----------|------------------------------------------|
| `string` | A plain string value |
| `integer` | A whole number (e.g. `42`, `-1`) |
| `float` | A decimal number (e.g. `3.14`) |
| `boolean` | `true` or `false` |
| `json` | A valid JSON object or array |
| `none` | Key does not exist or has expired |

## Errors

- `wrong number of arguments for 'type' command` — incorrect number of arguments.

## Examples

```
> SET counter 42
OK

> TYPE counter
integer

> SET ratio 3.14
OK

> TYPE ratio
float

> SET active true
OK

> TYPE active
boolean

> SET payload {"user":"alice","age":30}
OK

> TYPE payload
json

> SET greeting hello
OK

> TYPE greeting
string

> TYPE missing
none
```
2 changes: 2 additions & 0 deletions exc/command_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ func ExecuteCommand(command common.Cmd) ([]byte, error) {
return cmd.Keys(command)
case common.Config:
return cmd.ConfigCmd(command)
case common.Type:
return cmd.Type(command)
}
return []byte("-unsupported command\r\n"), UnsupportedCommand
}
Loading