Skip to content

Commit

Permalink
feat: add XRANGE command
Browse files Browse the repository at this point in the history
  • Loading branch information
mhughdo committed Jul 23, 2024
1 parent d02f64c commit 073039c
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 27 deletions.
1 change: 1 addition & 0 deletions pkg/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func NewCommandFactory(kv keyval.KV, cfg *config.Config) *CommandFactory {
"keys": &Keys{kv: kv},
"type": &TypeCmd{kv: kv},
"xadd": &XAdd{kv: kv},
"xrange": &XRange{kv: kv},
},
}
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/command/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package command

import "errors"

var (
ErrSyntaxError = errors.New("ERR syntax error")
ErrInvalidStreamID = errors.New("ERR Invalid stream ID specified as stream command argument")
)
119 changes: 119 additions & 0 deletions pkg/command/xrange.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package command

import (
"errors"
"fmt"
"math"
"strconv"
"strings"

"github.com/codecrafters-io/redis-starter-go/internal/client"
"github.com/codecrafters-io/redis-starter-go/pkg/keyval"
"github.com/codecrafters-io/redis-starter-go/pkg/resp"
)

type XRange struct {
kv keyval.KV
}

var (
ErrWrongArgCount = errors.New("wrong number of arguments for 'xrange' command")
)

func (x *XRange) Execute(_ *client.Client, wr *resp.Writer, args []*resp.Resp) error {
if err := x.validateArgs(args); err != nil {
return wr.WriteError(err)
}

streamID := args[0].String()
start, end, count, err := x.parseArgs(args)
if err != nil {
return wr.WriteError(err)
}

stream, err := x.kv.GetStream(streamID, false)
if err != nil {
return wr.WriteError(err)
}
if stream == nil {
return wr.WriteStringSlice([]string{})
}

entries := stream.Range(start, end, count)
result := x.formatEntries(entries)

return wr.WriteSlice(result)
}

func (x *XRange) validateArgs(args []*resp.Resp) error {
if len(args) < 3 {
return ErrWrongArgCount
}
if len(args) > 5 || len(args) == 4 {
return ErrSyntaxError
}
return nil
}

func (x *XRange) parseArgs(args []*resp.Resp) (start, end string, count uint64, err error) {
start, err = parseStreamID(args[1].String(), true)
if err != nil {
return
}
end, err = parseStreamID(args[2].String(), false)
if err != nil {
return
}

if len(args) == 5 {
if strings.ToUpper(args[3].String()) != "COUNT" {
err = ErrSyntaxError
return
}
count, err = args[4].Uint64()
if err != nil {
return
}
if count < 0 {
count = 0
}
}
return
}

func (x *XRange) formatEntries(entries []keyval.StreamEntry) []any {
var result []any
for _, entry := range entries {
entryResult := []any{entry.ID}
var entryFields []any
for _, field := range entry.Fields {
entryFields = append(entryFields, field[0], field[1])
}
entryResult = append(entryResult, entryFields)
result = append(result, entryResult)
}
return result
}

func parseStreamID(streamID string, start bool) (string, error) {
if streamID == "-" || streamID == "+" {
return streamID, nil
}

parts := strings.Split(streamID, "-")
if len(parts) > 2 {
return "", ErrSyntaxError
}

if len(parts) == 2 {
if _, err := strconv.ParseUint(parts[1], 10, 64); err != nil {
return "", ErrInvalidStreamID
}
return streamID, nil
}

if start {
return fmt.Sprintf("%s-0", parts[0]), nil
}
return fmt.Sprintf("%s-%d", parts[0], uint64(math.MaxUint64)), nil
}
4 changes: 2 additions & 2 deletions pkg/keyval/radix_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (t *RadixTree) updateLastID(id string) {
}
}

func (t *RadixTree) Range(startID, endID string, count uint64) ([]StreamEntry, error) {
func (t *RadixTree) Range(startID, endID string, count uint64) []StreamEntry {
inclusiveStart := true
inclusiveEnd := true
if strings.HasPrefix(startID, "(") {
Expand Down Expand Up @@ -245,7 +245,7 @@ func (t *RadixTree) Range(startID, endID string, count uint64) ([]StreamEntry, e

*resultPtr = result[:0]
entrySlicePool.Put(resultPtr)
return result, nil
return result
}

func (n *RadixNode) split(position int) {
Expand Down
30 changes: 6 additions & 24 deletions pkg/keyval/radix_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ func TestRadixTree_Range(t *testing.T) {
endID: "+",
wantEntries: nil,
count: 0,
wantErr: false,
},
{
name: "Range with one entry",
Expand All @@ -88,7 +87,6 @@ func TestRadixTree_Range(t *testing.T) {
wantEntries: []StreamEntry{
{ID: "1234567890-0", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Range with multiple entries",
Expand All @@ -109,7 +107,6 @@ func TestRadixTree_Range(t *testing.T) {
{ID: "1234567891-1", Fields: [][]string{{"field1", "value1"}}},
{ID: "1234567892-0", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Range with multiple small entries",
Expand Down Expand Up @@ -151,7 +148,6 @@ func TestRadixTree_Range(t *testing.T) {
{ID: "1234567891-0", Fields: [][]string{{"field1", "value1"}}},
{ID: "1234567891-1", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Range with limit",
Expand All @@ -169,7 +165,6 @@ func TestRadixTree_Range(t *testing.T) {
{ID: "1234567890-1", Fields: [][]string{{"field1", "value1"}}},
{ID: "1234567891-0", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Exclusive Range",
Expand All @@ -187,7 +182,6 @@ func TestRadixTree_Range(t *testing.T) {
{ID: "1234567891-1", Fields: [][]string{{"field1", "value1"}}},
{ID: "1234567892-0", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Exclusive Range on both sides",
Expand All @@ -204,7 +198,6 @@ func TestRadixTree_Range(t *testing.T) {
{ID: "1234567891-0", Fields: [][]string{{"field1", "value1"}}},
{ID: "1234567891-1", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Exclusive Range on both sides with limit",
Expand All @@ -220,7 +213,6 @@ func TestRadixTree_Range(t *testing.T) {
wantEntries: []StreamEntry{
{ID: "1234567891-0", Fields: [][]string{{"field1", "value1"}}},
},
wantErr: false,
},
{
name: "Range with no matching entries",
Expand All @@ -233,7 +225,6 @@ func TestRadixTree_Range(t *testing.T) {
endID: "1234567891-1",
count: 0,
wantEntries: nil,
wantErr: false,
},
}

Expand All @@ -244,11 +235,8 @@ func TestRadixTree_Range(t *testing.T) {
tree.AddEntry(entry.ID, entry.Fields)
}

gotEntries, err := tree.Range(tt.startID, tt.endID, tt.count)
if (err != nil) != tt.wantErr {
t.Errorf("Range() error = %v, wantErr %v", err, tt.wantErr)
return
}
gotEntries := tree.Range(tt.startID, tt.endID, tt.count)

if !compareStreamEntries(gotEntries, tt.wantEntries) {
t.Errorf("Range() gotEntries = %v, want %v", gotEntries, tt.wantEntries)
}
Expand Down Expand Up @@ -314,10 +302,7 @@ func BenchmarkRadixTree_Range(b *testing.B) {
end := start + rand.Intn(10000)
startID := fmt.Sprintf("%d", start)
endID := fmt.Sprintf("%d", end)
_, err := tree.Range(startID, endID, 0)
if err != nil {
b.Errorf("Range() error = %v", err)
}
tree.Range(startID, endID, 0)
}
}

Expand Down Expand Up @@ -485,7 +470,7 @@ func TestTrimBySize(t *testing.T) {
t.Errorf("TrimBySize(%d) returned %d, want %d", tt.maxSize, trimmed, tt.datasetSize-tt.expectedSize)
}

entries, _ := tree.Range("-", "+", uint64(tree.size))
entries := tree.Range("-", "+", uint64(tree.size))
if len(entries) != tt.expectedSize {
t.Errorf("Range returned %d entries, want %d", len(entries), tt.expectedSize)
}
Expand Down Expand Up @@ -546,7 +531,7 @@ func TestTrimByMinID(t *testing.T) {
t.Errorf("TrimByMinID(%s) returned %d, want %d", minID, trimmed, expectedTrimmed)
}

entries, _ := tree.Range("-", "+", uint64(tree.size))
entries := tree.Range("-", "+", uint64(tree.size))
if len(entries) != expectedSize {
t.Errorf("Range returned %d entries, want %d", len(entries), expectedSize)
}
Expand Down Expand Up @@ -660,10 +645,7 @@ func TestTrimByMinIDExplicit(t *testing.T) {
t.Errorf("TrimByMinID(%s) returned %d, want %d", tt.minID, trimmed, tt.expectedTrimmed)
}

entries, err := tree.Range("-", "+", uint64(len(tt.entries)))
if err != nil {
t.Fatalf("Failed to get range: %v", err)
}
entries := tree.Range("-", "+", uint64(len(tt.entries)))

if len(entries) != len(tt.expectedRemaining) {
t.Errorf("Range returned %d entries, want %d", len(entries), len(tt.expectedRemaining))
Expand Down
2 changes: 1 addition & 1 deletion pkg/keyval/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (s *Stream) AddEntry(entry StreamEntry) (string, error) {
return entry.ID, nil
}

func (s *Stream) Range(startID, endID string, count uint64) ([]StreamEntry, error) {
func (s *Stream) Range(startID, endID string, count uint64) []StreamEntry {
return s.tree.Range(startID, endID, count)
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/resp/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ func (w *Writer) WriteStringSlice(v []string) error {
return nil
}

func (w *Writer) WriteNestedStringSlice(v [][]string) error {
if err := w.WriteByte(Array); err != nil {
return err
}
if err := w.writeLen(len(v)); err != nil {
return err
}
for i := range v {
if err := w.WriteStringSlice(v[i]); err != nil {
return err
}
}
return nil
}

func (w *Writer) WriteSlice(v any) error {
reflectType := reflect.TypeOf(v)
if reflectType.Kind() == reflect.Ptr {
Expand Down

0 comments on commit 073039c

Please sign in to comment.