fix(storage): return Mongo List keys in key order like the memory store

This commit is contained in:
tiennm99 committed 2026-09-30 14:20:27 +07:00
1 parent 00eccd5f5b
commit 897e37ba80
3 files changed
+31 -4

No files matched your search

+3 -2
View File
@@ -40,8 +40,9 @@ var ErrInvalidModuleName = errors.New("storage: invalid module name")
// - PutVersioned writes only if the stored version equals expectedVersion, // - PutVersioned writes only if the stored version equals expectedVersion,
// then bumps it. expectedVersion == 0 means "create (or adopt a not-yet- // then bumps it. expectedVersion == 0 means "create (or adopt a not-yet-
// versioned key)". A mismatch returns ErrConflict. // versioned key)". A mismatch returns ErrConflict.
// - List returns the keys under a prefix; Scan returns those keys with their // - List returns the keys under a prefix, ordered by key ascending.
// values, ordered by key ascending. // - Scan returns the keys under a prefix with their values, ordered by key
// ascending.
type DocStore[T any] interface { type DocStore[T any] interface {
Get(ctx context.Context, id string) (val T, version int64, err error) Get(ctx context.Context, id string) (val T, version int64, err error)
Put(ctx context.Context, id string, val T) error Put(ctx context.Context, id string, val T) error
+5 -2
View File
@@ -153,7 +153,8 @@ func (s *mongoDocStore[T]) Delete(ctx context.Context, id string) error {
// List returns all document IDs whose key starts with prefix, via a half-open // List returns all document IDs whose key starts with prefix, via a half-open
// range scan on _id (reusing prefixSuccessor) so it uses the _id index and // range scan on _id (reusing prefixSuccessor) so it uses the _id index and
// avoids regex injection. Empty prefix returns the whole collection. // avoids regex injection. Keys come back sorted by _id, matching the memory
// store. Empty prefix returns the whole collection.
func (s *mongoDocStore[T]) List(ctx context.Context, prefix string) ([]string, error) { func (s *mongoDocStore[T]) List(ctx context.Context, prefix string) ([]string, error) {
if err := validatePrefix(prefix); err != nil { if err := validatePrefix(prefix); err != nil {
return nil, err return nil, err
@@ -162,7 +163,9 @@ func (s *mongoDocStore[T]) List(ctx context.Context, prefix string) ([]string, e
if prefix != "" { if prefix != "" {
filter[mongoIDField] = bson.M{"$gte": prefix, "$lt": prefixSuccessor(prefix)} filter[mongoIDField] = bson.M{"$gte": prefix, "$lt": prefixSuccessor(prefix)}
} }
cur, err := s.coll.Find(ctx, filter, options.Find().SetProjection(bson.M{mongoIDField: 1})) cur, err := s.coll.Find(ctx, filter, options.Find().
SetProjection(bson.M{mongoIDField: 1}).
SetSort(bson.D{{Key: mongoIDField, Value: 1}}))
if err != nil { if err != nil {
return nil, fmt.Errorf("mongo list %s prefix=%q: %w", s.module, prefix, err) return nil, fmt.Errorf("mongo list %s prefix=%q: %w", s.module, prefix, err)
} }
+23
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"slices"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -235,3 +236,25 @@ func TestMongoDocStore_ScanReturnsPayloadsInKeyOrder(t *testing.T) {
t.Fatalf("Scan all = %d docs, err %v", len(all), err) t.Fatalf("Scan all = %d docs, err %v", len(all), err)
} }
} }
// List returns keys in key order, like the memory store, so callers behave the
// same on both backends. Insertion order is deliberately reversed here.
func TestMongoDocStore_ListReturnsKeysInKeyOrder(t *testing.T) {
store, _, cleanup := mongoStore[portfolioLike](t, "coin")
defer cleanup()
ctx := context.Background()
for _, key := range []string{"user:3", "user:1", "other", "user:2"} {
if err := store.Put(ctx, key, portfolioLike{}); err != nil {
t.Fatalf("Put %s: %v", key, err)
}
}
keys, err := store.List(ctx, "user:")
if err != nil {
t.Fatalf("List: %v", err)
}
if want := []string{"user:1", "user:2", "user:3"}; !slices.Equal(keys, want) {
t.Errorf("List = %v, want %v", keys, want)
}
}