From 897e37ba8005b9b41776fe356158bd3d2db0fa51 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 30 Sep 2026 14:20:27 +0700 Subject: [PATCH] fix(storage): return Mongo List keys in key order like the memory store --- internal/storage/doc_store.go | 5 +++-- internal/storage/mongo_doc_store.go | 7 +++++-- internal/storage/mongo_doc_store_test.go | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/storage/doc_store.go b/internal/storage/doc_store.go index b10e8fd..58d1e67 100644 --- a/internal/storage/doc_store.go +++ b/internal/storage/doc_store.go @@ -40,8 +40,9 @@ var ErrInvalidModuleName = errors.New("storage: invalid module name") // - PutVersioned writes only if the stored version equals expectedVersion, // then bumps it. expectedVersion == 0 means "create (or adopt a not-yet- // versioned key)". A mismatch returns ErrConflict. -// - List returns the keys under a prefix; Scan returns those keys with their -// values, ordered by key ascending. +// - List returns the keys under a prefix, ordered by key ascending. +// - Scan returns the keys under a prefix with their values, ordered by key +// ascending. type DocStore[T any] interface { Get(ctx context.Context, id string) (val T, version int64, err error) Put(ctx context.Context, id string, val T) error diff --git a/internal/storage/mongo_doc_store.go b/internal/storage/mongo_doc_store.go index 3293faa..4428b18 100644 --- a/internal/storage/mongo_doc_store.go +++ b/internal/storage/mongo_doc_store.go @@ -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 // 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) { if err := validatePrefix(prefix); err != nil { return nil, err @@ -162,7 +163,9 @@ func (s *mongoDocStore[T]) List(ctx context.Context, prefix string) ([]string, e if 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 { return nil, fmt.Errorf("mongo list %s prefix=%q: %w", s.module, prefix, err) } diff --git a/internal/storage/mongo_doc_store_test.go b/internal/storage/mongo_doc_store_test.go index 1bdb085..f87f4e3 100644 --- a/internal/storage/mongo_doc_store_test.go +++ b/internal/storage/mongo_doc_store_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "slices" "sync" "testing" "time" @@ -235,3 +236,25 @@ func TestMongoDocStore_ScanReturnsPayloadsInKeyOrder(t *testing.T) { 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) + } +}