Skip to content

Commit 24e0b1d

Browse files
committed
simple support for DUMP and RESTORE
1 parent 360d575 commit 24e0b1d

File tree

5 files changed

+220
-32
lines changed

5 files changed

+220
-32
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Implemented commands:
3636
- Key
3737
- COPY
3838
- DEL
39+
- DUMP -- partly, only handles string keys
3940
- EXISTS
4041
- EXPIRE
4142
- EXPIREAT
@@ -50,6 +51,7 @@ Implemented commands:
5051
- RANDOMKEY -- see m.Seed(...)
5152
- RENAME
5253
- RENAMENX
54+
- RESTORE -- partly, only handles string keys
5355
- SCAN
5456
- TOUCH
5557
- TTL
@@ -301,10 +303,8 @@ Commands which will probably not be implemented:
301303
- ~~READONLY~~
302304
- ~~READWRITE~~
303305
- Key
304-
- ~~DUMP~~
305306
- ~~MIGRATE~~
306307
- ~~OBJECT~~
307-
- ~~RESTORE~~
308308
- ~~WAIT~~
309309
- Scripting
310310
- ~~FCALL / FCALL_RO *~~

cmd_generic.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func inMilliSeconds(t time.Time) int {
3232
func commandsGeneric(m *Miniredis) {
3333
m.srv.Register("COPY", m.cmdCopy)
3434
m.srv.Register("DEL", m.cmdDel)
35-
// DUMP
35+
m.srv.Register("DUMP", m.cmdDump)
3636
m.srv.Register("EXISTS", m.cmdExists)
3737
m.srv.Register("EXPIRE", makeCmdExpire(m, false, time.Second))
3838
m.srv.Register("EXPIREAT", makeCmdExpire(m, true, time.Second))
@@ -49,7 +49,7 @@ func commandsGeneric(m *Miniredis) {
4949
m.srv.Register("RANDOMKEY", m.cmdRandomkey)
5050
m.srv.Register("RENAME", m.cmdRename)
5151
m.srv.Register("RENAMENX", m.cmdRenamenx)
52-
// RESTORE
52+
m.srv.Register("RESTORE", m.cmdRestore)
5353
m.srv.Register("TOUCH", m.cmdTouch)
5454
m.srv.Register("TTL", m.cmdTTL)
5555
m.srv.Register("TYPE", m.cmdType)
@@ -315,6 +315,27 @@ func (m *Miniredis) cmdDel(c *server.Peer, cmd string, args []string) {
315315
})
316316
}
317317

318+
// DUMP
319+
func (m *Miniredis) cmdDump(c *server.Peer, cmd string, args []string) {
320+
if !m.isValidCMD(c, cmd, args, exactly(1)) {
321+
return
322+
}
323+
324+
key := args[0]
325+
326+
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
327+
db := m.db(ctx.selectedDB)
328+
keyType, exists := db.keys[key]
329+
if !exists {
330+
c.WriteNull()
331+
} else if keyType != keyTypeString {
332+
c.WriteError(msgWrongType)
333+
} else {
334+
c.WriteBulk(db.stringGet(key))
335+
}
336+
})
337+
}
338+
318339
// TYPE
319340
func (m *Miniredis) cmdType(c *server.Peer, cmd string, args []string) {
320341
if !m.isValidCMD(c, cmd, args, exactly(1)) {
@@ -487,6 +508,78 @@ func (m *Miniredis) cmdRenamenx(c *server.Peer, cmd string, args []string) {
487508
})
488509
}
489510

511+
type restoreOpts struct {
512+
key string
513+
serializedValue string
514+
rawTtl string
515+
replace bool
516+
absTtl bool
517+
}
518+
519+
func restoreParse(args []string) *restoreOpts {
520+
var opts restoreOpts
521+
522+
opts.key, opts.rawTtl, opts.serializedValue, args = args[0], args[1], args[2], args[3:]
523+
524+
for len(args) > 0 {
525+
switch arg := strings.ToUpper(args[0]); arg {
526+
case "REPLACE":
527+
opts.replace = true
528+
case "ABSTTL":
529+
opts.absTtl = true
530+
default:
531+
return nil
532+
}
533+
534+
args = args[1:]
535+
}
536+
537+
return &opts
538+
}
539+
540+
// RESTORE
541+
func (m *Miniredis) cmdRestore(c *server.Peer, cmd string, args []string) {
542+
if !m.isValidCMD(c, cmd, args, atLeast(3)) {
543+
return
544+
}
545+
546+
var opts = restoreParse(args)
547+
if opts == nil {
548+
setDirty(c)
549+
c.WriteError(msgSyntaxError)
550+
return
551+
}
552+
553+
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
554+
db := m.db(ctx.selectedDB)
555+
556+
_, keyExists := db.keys[opts.key]
557+
if keyExists && !opts.replace {
558+
setDirty(c)
559+
c.WriteError("BUSYKEY Target key name already exists.")
560+
return
561+
}
562+
563+
ttl, err := strconv.Atoi(opts.rawTtl)
564+
if err != nil || ttl < 0 {
565+
c.WriteError(msgInvalidInt)
566+
return
567+
}
568+
569+
db.stringSet(opts.key, opts.serializedValue)
570+
571+
if ttl != 0 {
572+
if opts.absTtl {
573+
db.ttl[opts.key] = m.at(ttl, time.Millisecond)
574+
} else {
575+
db.ttl[opts.key] = time.Duration(ttl) * time.Millisecond
576+
}
577+
}
578+
579+
c.WriteOK()
580+
})
581+
}
582+
490583
type scanOpts struct {
491584
cursor int
492585
count int

cmd_generic_test.go

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,39 @@ func TestUnlink(t *testing.T) {
287287
})
288288
}
289289

290+
func TestDump(t *testing.T) {
291+
s, c := runWithClient(t)
292+
s.Set("existing-key", "value")
293+
s.HSet("set-key", "a", "b")
294+
295+
t.Run("parse errors", func(t *testing.T) {
296+
mustDo(t, c,
297+
"DUMP",
298+
proto.Error(errWrongNumber("dump")))
299+
300+
mustDo(t, c,
301+
"DUMP", "key", "extra",
302+
proto.Error(errWrongNumber("dump")))
303+
})
304+
305+
t.Run("dump missing key", func(t *testing.T) {
306+
mustNil(t, c,
307+
"DUMP", "missing-key")
308+
})
309+
310+
t.Run("dump existing key", func(t *testing.T) {
311+
mustDo(t, c,
312+
"DUMP", "existing-key",
313+
proto.String("value"))
314+
})
315+
316+
t.Run("dump wrong key type", func(t *testing.T) {
317+
mustDo(t, c,
318+
"DUMP", "set-key",
319+
proto.Error(msgWrongType))
320+
})
321+
}
322+
290323
func TestType(t *testing.T) {
291324
s, c := runWithClient(t)
292325

@@ -316,11 +349,11 @@ func TestType(t *testing.T) {
316349
t.Run("errors", func(t *testing.T) {
317350
mustDo(t, c,
318351
"TYPE",
319-
proto.Error("usage error"),
352+
proto.Error(errWrongNumber("type")),
320353
)
321354
mustDo(t, c,
322355
"TYPE", "spurious", "arguments",
323-
proto.Error("usage error"),
356+
proto.Error(errWrongNumber("type")),
324357
)
325358
})
326359

@@ -606,6 +639,67 @@ func TestRename(t *testing.T) {
606639
})
607640
}
608641

642+
func TestRestore(t *testing.T) {
643+
s, c := runWithClient(t)
644+
s.Set("existing-key", "value")
645+
646+
t.Run("busy key error", func(t *testing.T) {
647+
mustDo(t, c,
648+
"RESTORE", "existing-key", "0", "other-value",
649+
proto.Error("BUSYKEY Target key name already exists."))
650+
651+
// Value hasn't changed
652+
s.CheckGet(t, "existing-key", "value")
653+
})
654+
655+
t.Run("overwrite existing key", func(t *testing.T) {
656+
mustOK(t, c,
657+
"RESTORE", "existing-key", "0", "new-value", "REPLACE")
658+
659+
s.CheckGet(t, "existing-key", "new-value")
660+
})
661+
662+
t.Run("restore new key with no ttl", func(t *testing.T) {
663+
mustOK(t, c, "RESTORE", "key-a", "0", "value-a")
664+
665+
s.CheckGet(t, "key-a", "value-a")
666+
equals(t, time.Duration(0), s.TTL("key-a"))
667+
})
668+
669+
t.Run("restore new key with regular ttl", func(t *testing.T) {
670+
mustOK(t, c, "RESTORE", "key-b", "50", "value-b")
671+
672+
s.CheckGet(t, "key-b", "value-b")
673+
equals(t, 50*time.Millisecond, s.TTL("key-b"))
674+
})
675+
676+
t.Run("restore new key with absolute ttl", func(t *testing.T) {
677+
s.SetTime(time.UnixMilli(1234567890))
678+
mustOK(t, c, "RESTORE", "key-c", "1234577890", "value-c", "ABSTTL")
679+
680+
s.CheckGet(t, "key-c", "value-c")
681+
equals(t, 10000*time.Millisecond, s.TTL("key-c"))
682+
})
683+
684+
t.Run("parse errors", func(t *testing.T) {
685+
mustDo(t, c,
686+
"RESTORE",
687+
proto.Error(errWrongNumber("restore")))
688+
689+
mustDo(t, c,
690+
"RESTORE", "key", "-1", "serialized-value", "BADARG",
691+
proto.Error(msgSyntaxError))
692+
693+
mustDo(t, c,
694+
"RESTORE", "key", "-1", "serialized-value",
695+
proto.Error(msgInvalidInt))
696+
697+
mustDo(t, c,
698+
"RESTORE", "key", "argh", "serialized-value",
699+
proto.Error(msgInvalidInt))
700+
})
701+
}
702+
609703
func TestScan(t *testing.T) {
610704
s, c := runWithClient(t)
611705

miniredis.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,15 +373,15 @@ func (m *Miniredis) Server() *server.Server {
373373
return m.srv
374374
}
375375

376-
// Dump returns a text version of the selected DB, usable for debugging.
376+
// DebugDump returns a text version of the selected DB, usable for debugging.
377377
//
378-
// Dump limits the maximum length of each key:value to "DumpMaxLineLen" characters.
378+
// DebugDump limits the maximum length of each key:value to "DumpMaxLineLen" characters.
379379
// To increase that, call something like:
380380
//
381381
// miniredis.DumpMaxLineLen = 1024
382382
// mr, _ = miniredis.Run()
383-
// mr.Dump()
384-
func (m *Miniredis) Dump() string {
383+
// mr.DebugDump()
384+
func (m *Miniredis) DebugDump() string {
385385
m.Lock()
386386
defer m.Unlock()
387387

0 commit comments

Comments
 (0)