Skip to content

Commit ca4b083

Browse files
h3n4lclaude
andauthored
feat: add db.getCollectionInfos() operation support (#7)
Add support for the db.getCollectionInfos() command which returns collection metadata including name, type, options, and info fields. Supports optional filter argument: - db.getCollectionInfos() - db.getCollectionInfos({ name: "users" }) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 79f0814 commit ca4b083

5 files changed

Lines changed: 165 additions & 3 deletions

File tree

executor.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func executeOperation(ctx context.Context, client *mongo.Client, database string
7171
return executeShowCollections(ctx, client, database)
7272
case opGetCollectionNames:
7373
return executeGetCollectionNames(ctx, client, database)
74+
case opGetCollectionInfos:
75+
return executeGetCollectionInfos(ctx, client, database, op)
7476
default:
7577
return nil, &UnsupportedOperationError{
7678
Operation: statement,
@@ -251,3 +253,40 @@ func executeShowCollections(ctx context.Context, client *mongo.Client, database
251253
func executeGetCollectionNames(ctx context.Context, client *mongo.Client, database string) (*Result, error) {
252254
return executeShowCollections(ctx, client, database)
253255
}
256+
257+
// executeGetCollectionInfos executes a db.getCollectionInfos() command.
258+
func executeGetCollectionInfos(ctx context.Context, client *mongo.Client, database string, op *mongoOperation) (*Result, error) {
259+
filter := op.filter
260+
if filter == nil {
261+
filter = bson.D{}
262+
}
263+
264+
cursor, err := client.Database(database).ListCollections(ctx, filter)
265+
if err != nil {
266+
return nil, fmt.Errorf("list collections failed: %w", err)
267+
}
268+
defer func() { _ = cursor.Close(ctx) }()
269+
270+
var rows []string
271+
for cursor.Next(ctx) {
272+
var doc bson.M
273+
if err := cursor.Decode(&doc); err != nil {
274+
return nil, fmt.Errorf("decode failed: %w", err)
275+
}
276+
277+
jsonBytes, err := bson.MarshalExtJSONIndent(doc, false, false, "", " ")
278+
if err != nil {
279+
return nil, fmt.Errorf("marshal failed: %w", err)
280+
}
281+
rows = append(rows, string(jsonBytes))
282+
}
283+
284+
if err := cursor.Err(); err != nil {
285+
return nil, fmt.Errorf("cursor error: %w", err)
286+
}
287+
288+
return &Result{
289+
Rows: rows,
290+
RowCount: len(rows),
291+
}, nil
292+
}

executor_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,3 +1362,75 @@ func TestAggregateMultiFieldJoin(t *testing.T) {
13621362
require.Contains(t, result.Rows[0], `"variation"`)
13631363
require.NotContains(t, result.Rows[0], `"_id"`)
13641364
}
1365+
1366+
func TestGetCollectionInfos(t *testing.T) {
1367+
client, cleanup := setupTestContainer(t)
1368+
defer cleanup()
1369+
1370+
ctx := context.Background()
1371+
1372+
// Create collections by inserting documents
1373+
_, err := client.Database("testdb").Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
1374+
require.NoError(t, err)
1375+
_, err = client.Database("testdb").Collection("orders").InsertOne(ctx, bson.M{"item": "book"})
1376+
require.NoError(t, err)
1377+
1378+
gc := gomongo.NewClient(client)
1379+
1380+
// Test without filter - should return all collections
1381+
result, err := gc.Execute(ctx, "testdb", "db.getCollectionInfos()")
1382+
require.NoError(t, err)
1383+
require.NotNil(t, result)
1384+
require.Equal(t, 2, result.RowCount)
1385+
1386+
// Verify that results contain collection info structure
1387+
for _, row := range result.Rows {
1388+
require.Contains(t, row, `"name"`)
1389+
require.Contains(t, row, `"type"`)
1390+
}
1391+
}
1392+
1393+
func TestGetCollectionInfosWithFilter(t *testing.T) {
1394+
client, cleanup := setupTestContainer(t)
1395+
defer cleanup()
1396+
1397+
ctx := context.Background()
1398+
1399+
// Create collections by inserting documents
1400+
_, err := client.Database("testdb").Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
1401+
require.NoError(t, err)
1402+
_, err = client.Database("testdb").Collection("orders").InsertOne(ctx, bson.M{"item": "book"})
1403+
require.NoError(t, err)
1404+
1405+
gc := gomongo.NewClient(client)
1406+
1407+
// Test with filter - should return only matching collection
1408+
result, err := gc.Execute(ctx, "testdb", `db.getCollectionInfos({ name: "users" })`)
1409+
require.NoError(t, err)
1410+
require.NotNil(t, result)
1411+
require.Equal(t, 1, result.RowCount)
1412+
1413+
// Verify that the returned collection is "users"
1414+
require.Contains(t, result.Rows[0], `"name": "users"`)
1415+
require.Contains(t, result.Rows[0], `"type": "collection"`)
1416+
}
1417+
1418+
func TestGetCollectionInfosEmptyResult(t *testing.T) {
1419+
client, cleanup := setupTestContainer(t)
1420+
defer cleanup()
1421+
1422+
ctx := context.Background()
1423+
1424+
// Create a collection
1425+
_, err := client.Database("testdb").Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
1426+
require.NoError(t, err)
1427+
1428+
gc := gomongo.NewClient(client)
1429+
1430+
// Test with filter that matches no collections
1431+
result, err := gc.Execute(ctx, "testdb", `db.getCollectionInfos({ name: "nonexistent" })`)
1432+
require.NoError(t, err)
1433+
require.NotNil(t, result)
1434+
require.Equal(t, 0, result.RowCount)
1435+
require.Empty(t, result.Rows)
1436+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.24.5
44

55
require (
66
github.com/antlr4-go/antlr/v4 v4.13.1
7-
github.com/bytebase/parser v0.0.0-20260113083029-8ac2a658441d
7+
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd
88
github.com/google/uuid v1.6.0
99
github.com/stretchr/testify v1.11.1
1010
github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
88
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
99
github.com/bytebase/antlr/v4 v4.0.0-20240827034948-8c385f108920 h1:IfmPt5o5R70NKtOrs+QHOoCgViYZelZysGxVBvV4ybA=
1010
github.com/bytebase/antlr/v4 v4.0.0-20240827034948-8c385f108920/go.mod h1:ykhjIPiv0IWpu3OGXCHdz2eUSe8UNGGD6baqjs8jSuU=
11-
github.com/bytebase/parser v0.0.0-20260113083029-8ac2a658441d h1:v6jYI26RpZgfLxlrRbGIjoMsrLtirZp/dQvPfZlA95E=
12-
github.com/bytebase/parser v0.0.0-20260113083029-8ac2a658441d/go.mod h1:jeak/EfutSOAuWKvrFIT2IZunhWprM7oTFBRgZ9RCxo=
11+
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd h1:JCEEza5T4CTNWZuwHe6/7mqG7Qg+q2ZiHP00UtW+NtQ=
12+
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd/go.mod h1:jeak/EfutSOAuWKvrFIT2IZunhWprM7oTFBRgZ9RCxo=
1313
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
1414
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
1515
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=

translator.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
opShowDatabases
2121
opShowCollections
2222
opGetCollectionNames
23+
opGetCollectionInfos
2324
)
2425

2526
// mongoOperation represents a parsed MongoDB operation.
@@ -86,6 +87,9 @@ func (v *mongoShellVisitor) visitDbStatement(ctx mongodb.IDbStatementContext) {
8687
v.visitCollectionOperation(c)
8788
case *mongodb.GetCollectionNamesContext:
8889
v.operation.opType = opGetCollectionNames
90+
case *mongodb.GetCollectionInfosContext:
91+
v.operation.opType = opGetCollectionInfos
92+
v.extractGetCollectionInfosArgs(c)
8993
}
9094
}
9195

@@ -121,6 +125,53 @@ func (v *mongoShellVisitor) VisitGetCollectionNames(_ *mongodb.GetCollectionName
121125
return nil
122126
}
123127

128+
func (v *mongoShellVisitor) VisitGetCollectionInfos(ctx *mongodb.GetCollectionInfosContext) any {
129+
v.operation.opType = opGetCollectionInfos
130+
v.extractGetCollectionInfosArgs(ctx)
131+
return nil
132+
}
133+
134+
func (v *mongoShellVisitor) extractGetCollectionInfosArgs(ctx *mongodb.GetCollectionInfosContext) {
135+
args := ctx.Arguments()
136+
if args == nil {
137+
return
138+
}
139+
140+
argsCtx, ok := args.(*mongodb.ArgumentsContext)
141+
if !ok {
142+
return
143+
}
144+
145+
allArgs := argsCtx.AllArgument()
146+
if len(allArgs) == 0 {
147+
return
148+
}
149+
150+
// First argument is the filter (optional)
151+
firstArg, ok := allArgs[0].(*mongodb.ArgumentContext)
152+
if !ok {
153+
return
154+
}
155+
156+
valueCtx := firstArg.Value()
157+
if valueCtx == nil {
158+
return
159+
}
160+
161+
docValue, ok := valueCtx.(*mongodb.DocumentValueContext)
162+
if !ok {
163+
v.err = fmt.Errorf("getCollectionInfos() filter must be a document")
164+
return
165+
}
166+
167+
filter, err := convertDocument(docValue.Document())
168+
if err != nil {
169+
v.err = fmt.Errorf("invalid filter: %w", err)
170+
return
171+
}
172+
v.operation.filter = filter
173+
}
174+
124175
func (v *mongoShellVisitor) extractCollectionName(ctx mongodb.ICollectionAccessContext) string {
125176
switch c := ctx.(type) {
126177
case *mongodb.DotAccessContext:

0 commit comments

Comments
 (0)