Skip to content

Commit 344c187

Browse files
authored
Implement legacy collection enumeration. (#120)
GODRIVER-492 Change-Id: Iafd2527267b470a582925cc52fdd93bd7f7d7a9e
1 parent 8c736ad commit 344c187

File tree

7 files changed

+295
-43
lines changed

7 files changed

+295
-43
lines changed

mongo/client_internal_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/mongodb/mongo-go-driver/mongo/writeconcern"
2828
"github.com/mongodb/mongo-go-driver/x/mongo/driver/session"
2929
"github.com/mongodb/mongo-go-driver/x/mongo/driver/uuid"
30+
"github.com/mongodb/mongo-go-driver/x/network/connstring"
3031
)
3132

3233
func createTestClient(t *testing.T) *Client {
@@ -41,6 +42,18 @@ func createTestClient(t *testing.T) *Client {
4142
}
4243
}
4344

45+
func createTestClientWithConnstring(t *testing.T, cs connstring.ConnString) *Client {
46+
id, _ := uuid.New()
47+
return &Client{
48+
id: id,
49+
topology: testutil.TopologyWithConnString(t, cs),
50+
connString: cs,
51+
readPreference: readpref.Primary(),
52+
clock: &session.ClusterClock{},
53+
registry: bson.DefaultRegistry,
54+
}
55+
}
56+
4457
func skipIfBelow30(t *testing.T) {
4558
serverVersion, err := getServerVersion(createTestDatabase(t, nil))
4659
require.NoError(t, err)

mongo/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func (db *Database) ListCollections(ctx context.Context, filter interface{}, opt
232232
cmd := command.ListCollections{
233233
DB: db.name,
234234
Filter: filterDoc,
235-
ReadPref: db.readPreference,
235+
ReadPref: readpref.Primary(), // list collections must be run on a primary by default
236236
Session: sess,
237237
Clock: db.client.clock,
238238
}

mongo/database_internal_test.go

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"github.com/mongodb/mongo-go-driver/mongo/readpref"
2222
"github.com/mongodb/mongo-go-driver/mongo/writeconcern"
2323
"github.com/mongodb/mongo-go-driver/x/bsonx"
24+
"github.com/mongodb/mongo-go-driver/x/network/connstring"
25+
"github.com/mongodb/mongo-go-driver/x/network/description"
2426
"github.com/stretchr/testify/require"
2527
)
2628

@@ -206,7 +208,7 @@ func verifyListCollections(cursor Cursor, uncappedName string, cappedName string
206208
var cappedFound bool
207209

208210
for cursor.Next(context.Background()) {
209-
next := bsonx.Doc{}
211+
next := &bsonx.Doc{}
210212
err = cursor.Decode(next)
211213
if err != nil {
212214
return err
@@ -223,9 +225,9 @@ func verifyListCollections(cursor Cursor, uncappedName string, cappedName string
223225

224226
elemName := elem.StringValue()
225227

226-
if elemName != uncappedName && elemName != cappedName {
227-
return fmt.Errorf("incorrect collection name. got: %s. wanted: %s or %s", elemName, uncappedName,
228-
cappedName)
228+
// legacy servers can return an indexes collection that shouldn't be considered here
229+
if elemName != cappedName && elemName != uncappedName {
230+
continue
229231
}
230232

231233
if elemName == uncappedName && !uncappedFound {
@@ -257,19 +259,16 @@ func verifyListCollections(cursor Cursor, uncappedName string, cappedName string
257259
return nil
258260
}
259261

260-
func listCollectionsTest(db *Database, cappedOnly bool) error {
261-
uncappedName, cappedName, err := setupListCollectionsDb(db)
262-
if err != nil {
263-
return err
264-
}
265-
262+
func listCollectionsTest(db *Database, cappedOnly bool, cappedName, uncappedName string) error {
266263
var filter bsonx.Doc
267264
if cappedOnly {
268265
filter = bsonx.Doc{{"options.capped", bsonx.Boolean(true)}}
269266
}
270267

268+
var cursor Cursor
269+
var err error
271270
for i := 0; i < 10; i++ {
272-
cursor, err := db.ListCollections(context.Background(), filter)
271+
cursor, err = db.ListCollections(context.Background(), filter)
273272
if err != nil {
274273
return err
275274
}
@@ -283,43 +282,69 @@ func listCollectionsTest(db *Database, cappedOnly bool) error {
283282
return err // all tests failed
284283
}
285284

286-
func TestDatabase_ListCollections(t *testing.T) {
287-
// TODO(GODRIVER-492) - implement legacy list collections
288-
skipIfBelow32(t)
285+
// get the connection string for a direct connection to a secondary in a replica set
286+
func getSecondaryConnString(t *testing.T) connstring.ConnString {
287+
topo := testutil.Topology(t)
288+
for _, server := range topo.Description().Servers {
289+
if server.Kind != description.RSSecondary {
290+
continue
291+
}
289292

290-
rpPrimary := readpref.Primary()
291-
rpSecondary := readpref.Secondary()
293+
fullAddr := "mongodb://" + server.Addr.String() + "/?connect=direct"
294+
cs, err := connstring.Parse(fullAddr)
295+
require.NoError(t, err)
296+
return cs
297+
}
292298

299+
t.Fatalf("no secondary found for %s", t.Name())
300+
return connstring.ConnString{}
301+
}
302+
303+
func TestDatabase_ListCollections(t *testing.T) {
293304
var listCollectionsTable = []struct {
294305
name string
295306
expectedTopology string
296307
cappedOnly bool
297-
rp *readpref.ReadPref
308+
direct bool
298309
}{
299-
{"standalone_nofilter", "server", false, rpPrimary},
300-
{"standalone_filter", "server", true, rpPrimary},
301-
{"replicaset_nofilter", "replica_set", false, rpPrimary},
302-
{"replicaset_filter", "replica_set", true, rpPrimary},
303-
{"replicaset_secondary_nofilter", "replica_set", false, rpSecondary},
304-
{"replicaset_secondary_filter", "replica_set", true, rpSecondary},
305-
{"sharded_nofilter", "sharded_cluster", false, rpPrimary},
306-
{"sharded_filter", "sharded_cluster", true, rpPrimary},
310+
{"standalone_nofilter", "server", false, false},
311+
{"standalone_filter", "server", true, false},
312+
{"replicaset_nofilter", "replica_set", false, false},
313+
{"replicaset_filter", "replica_set", true, false},
314+
{"replicaset_secondary_nofilter", "replica_set", false, true},
315+
{"replicaset_secondary_filter", "replica_set", true, true},
316+
{"sharded_nofilter", "sharded_cluster", false, false},
317+
{"sharded_filter", "sharded_cluster", true, false},
307318
}
308319

309320
for _, tt := range listCollectionsTable {
310321
t.Run(tt.name, func(t *testing.T) {
311-
if os.Getenv("topology") != tt.expectedTopology {
322+
if os.Getenv("TOPOLOGY") != tt.expectedTopology {
312323
t.Skip()
313324
}
314-
dbName := tt.name
315-
db := createTestDatabase(t, &dbName, options.Database().SetReadPreference(tt.rp))
316325

326+
createDb := createTestDatabase(t, &tt.name, options.Database().SetWriteConcern(wcMajority))
317327
defer func() {
318-
err := db.Drop(context.Background())
328+
err := createDb.Drop(context.Background())
319329
require.NoError(t, err)
320330
}()
321331

322-
err := listCollectionsTest(db, tt.cappedOnly)
332+
uncappedName, cappedName, err := setupListCollectionsDb(createDb)
333+
require.NoError(t, err)
334+
335+
var cs connstring.ConnString
336+
if tt.direct {
337+
// TODO(GODRIVER-641) - correctly set read preference on direct connections for OP_MSG
338+
t.Skip()
339+
cs = getSecondaryConnString(t)
340+
} else {
341+
cs = testutil.ConnString(t)
342+
}
343+
344+
client := createTestClientWithConnstring(t, cs)
345+
db := client.Database(tt.name)
346+
347+
err = listCollectionsTest(db, tt.cappedOnly, cappedName, uncappedName)
323348
require.NoError(t, err)
324349
})
325350
}

x/mongo/driver/list_collections.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@ package driver
99
import (
1010
"context"
1111

12+
"errors"
1213
"github.com/mongodb/mongo-go-driver/mongo/options"
1314
"github.com/mongodb/mongo-go-driver/x/bsonx"
1415
"github.com/mongodb/mongo-go-driver/x/mongo/driver/session"
1516
"github.com/mongodb/mongo-go-driver/x/mongo/driver/topology"
1617
"github.com/mongodb/mongo-go-driver/x/mongo/driver/uuid"
1718
"github.com/mongodb/mongo-go-driver/x/network/command"
19+
"github.com/mongodb/mongo-go-driver/x/network/connection"
1820
"github.com/mongodb/mongo-go-driver/x/network/description"
1921
)
2022

23+
// ErrFilterType is thrown when a non-string filter is specified.
24+
var ErrFilterType = errors.New("filter must be a string")
25+
2126
// ListCollections handles the full cycle dispatch and execution of a listCollections command against the provided
2227
// topology.
2328
func ListCollections(
@@ -41,6 +46,10 @@ func ListCollections(
4146
}
4247
defer conn.Close()
4348

49+
if ss.Description().WireVersion.Max < 3 {
50+
return legacyListCollections(ctx, cmd, ss, conn)
51+
}
52+
4453
rp, err := getReadPrefBasedOnTransaction(cmd.ReadPref, cmd.Session)
4554
if err != nil {
4655
return nil, err
@@ -67,3 +76,49 @@ func ListCollections(
6776

6877
return c, err
6978
}
79+
80+
func legacyListCollections(
81+
ctx context.Context,
82+
cmd command.ListCollections,
83+
ss *topology.SelectedServer,
84+
conn connection.Connection,
85+
) (command.Cursor, error) {
86+
filter, err := transformFilter(cmd.Filter, cmd.DB)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
findCmd := command.Find{
92+
NS: command.NewNamespace(cmd.DB, "system.namespaces"),
93+
ReadPref: cmd.ReadPref,
94+
Filter: filter,
95+
}
96+
97+
// don't need registry because it's used to create BSON docs for find options that don't exist in this case
98+
c, err := legacyFind(ctx, findCmd, nil, ss, conn)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return topology.NewListCollectionsCursor(c), nil
104+
}
105+
106+
// modify the user-supplied filter to prefix the "name" field with the database name.
107+
// returns the original filter if the name field is not present or a copy with the modified name field if it is
108+
func transformFilter(filter bsonx.Doc, dbName string) (bsonx.Doc, error) {
109+
if filter == nil {
110+
return filter, nil
111+
}
112+
113+
if nameVal, err := filter.LookupErr("name"); err == nil {
114+
name, ok := nameVal.StringValueOK()
115+
if !ok {
116+
return nil, ErrFilterType
117+
}
118+
119+
filterCopy := filter.Copy()
120+
filterCopy.Set("name", bsonx.String(dbName+"."+name))
121+
return filterCopy, nil
122+
}
123+
return filter, nil
124+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (C) MongoDB, Inc. 2017-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package driver
8+
9+
import (
10+
"github.com/mongodb/mongo-go-driver/x/bsonx"
11+
"github.com/stretchr/testify/require"
12+
"testing"
13+
)
14+
15+
func TestListCollections(t *testing.T) {
16+
dbName := "db"
17+
noNameFilter := bsonx.Doc{
18+
{"foo", bsonx.String("bar")},
19+
}
20+
nonStringFilter := bsonx.Doc{
21+
{"name", bsonx.Int32(1)},
22+
}
23+
nameFilter := bsonx.Doc{
24+
{"name", bsonx.String("coll")},
25+
}
26+
modifiedFilter := bsonx.Doc{
27+
{"name", bsonx.String(dbName + ".coll")},
28+
}
29+
30+
t.Run("TestTransformFilter", func(t *testing.T) {
31+
testCases := []struct {
32+
name string
33+
filter bsonx.Doc
34+
expectedFilter bsonx.Doc
35+
err error
36+
}{
37+
{"TestNilFilter", nil, nil, nil},
38+
{"TestNoName", noNameFilter, noNameFilter, nil},
39+
{"TestNonStringName", nonStringFilter, nil, ErrFilterType},
40+
{"TestName", nameFilter, modifiedFilter, nil},
41+
}
42+
43+
for _, tc := range testCases {
44+
t.Run(tc.name, func(t *testing.T) {
45+
newFilter, err := transformFilter(tc.filter, dbName)
46+
require.Equal(t, tc.err, err)
47+
require.Equal(t, tc.expectedFilter, newFilter)
48+
})
49+
}
50+
})
51+
}

0 commit comments

Comments
 (0)