diff --git a/internal/db/db.go b/internal/db/db.go index e38e40616c8a3f1030b5df1f23c4b4ae0dfef36e..49ad1ee8207f8e0e3e018c62066adccf3f383a46 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -270,7 +270,15 @@ func (t *Txn) Iterate(opts IterateOptions, fn func(Item) error) error { defer it.Close() if len(opts.Prefix) > 0 { - for it.Seek(opts.Prefix); it.ValidForPrefix(opts.Prefix); it.Next() { + if opts.Reverse { + // Seek to the end of the prefix range by appending 0xFF + seekKey := append(opts.Prefix, 0xFF) + it.Seek(seekKey) + } else { + it.Seek(opts.Prefix) + } + + for ; it.ValidForPrefix(opts.Prefix); it.Next() { if err := fn(Item{item: it.Item()}); err != nil { if errors.Is(err, ErrTxnAborted) { return nil diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 51d105f6fe10ea31e71f987f1a063544ba90a157..a4218e83fa43912a7b0bc816fbd1eca1e5f70dfd 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -589,3 +589,105 @@ func TestEncodingHelpers(t *testing.T) { t.Fatalf("unexpected encodeHex: %q", hex) } } + +func TestIterateReversePrefixOrder(t *testing.T) { + database := openTestDB(t) + + keys := [][]byte{ + []byte("dir/abc/archived/0001/sid1"), + []byte("dir/abc/archived/0002/sid2"), + []byte("dir/abc/archived/0003/sid3"), + []byte("dir/xyz/archived/0001/sid4"), + []byte("other/key"), + } + + err := database.Update(context.Background(), func(txn *Txn) error { + for _, key := range keys { + if err := txn.Set(key, key); err != nil { + return err + } + } + return nil + }) + if err != nil { + t.Fatalf("populate keys: %v", err) + } + + t.Run("ReverseWithPrefix", func(t *testing.T) { + var seen []string + err := database.View(context.Background(), func(txn *Txn) error { + return txn.Iterate(IterateOptions{ + Prefix: []byte("dir/abc/archived"), + Reverse: true, + }, func(item Item) error { + seen = append(seen, item.KeyString()) + return nil + }) + }) + if err != nil { + t.Fatalf("reverse prefix iteration: %v", err) + } + + expected := []string{ + "dir/abc/archived/0003/sid3", + "dir/abc/archived/0002/sid2", + "dir/abc/archived/0001/sid1", + } + if len(seen) != len(expected) { + t.Fatalf("unexpected number of items: got %d want %d", len(seen), len(expected)) + } + for i, want := range expected { + if seen[i] != want { + t.Fatalf("unexpected key at %d: got %q want %q", i, seen[i], want) + } + } + }) + + t.Run("ForwardWithPrefix", func(t *testing.T) { + var seen []string + err := database.View(context.Background(), func(txn *Txn) error { + return txn.Iterate(IterateOptions{ + Prefix: []byte("dir/abc/archived"), + }, func(item Item) error { + seen = append(seen, item.KeyString()) + return nil + }) + }) + if err != nil { + t.Fatalf("forward prefix iteration: %v", err) + } + + expected := []string{ + "dir/abc/archived/0001/sid1", + "dir/abc/archived/0002/sid2", + "dir/abc/archived/0003/sid3", + } + if len(seen) != len(expected) { + t.Fatalf("unexpected number of items: got %d want %d", len(seen), len(expected)) + } + for i, want := range expected { + if seen[i] != want { + t.Fatalf("unexpected key at %d: got %q want %q", i, seen[i], want) + } + } + }) + + t.Run("EmptyPrefixReverse", func(t *testing.T) { + var count int + err := database.View(context.Background(), func(txn *Txn) error { + return txn.Iterate(IterateOptions{ + Prefix: []byte("nonexistent/prefix"), + Reverse: true, + }, func(item Item) error { + count++ + return nil + }) + }) + if err != nil { + t.Fatalf("empty prefix iteration: %v", err) + } + if count != 0 { + t.Fatalf("expected 0 items for nonexistent prefix, got %d", count) + } + }) +}