Skip to content

Commit d2ea344

Browse files
h3n4lclaude
andauthored
feat: implement remaining M3 administrative commands (#24)
* feat: implement remaining M3 administrative commands Add 15 remaining M3 commands via RunCommand and Go driver APIs: - Database info: db.stats(), db.serverStatus(), db.serverBuildInfo(), db.version(), db.hostInfo(), db.listCommands() - Collection info: collection.stats(), collection.dataSize(), collection.storageSize(), collection.totalIndexSize(), collection.totalSize(), collection.isCapped() - Collection admin: collection.validate(), collection.latencyStats() - Index management: collection.createIndexes() This completes the full M3 milestone, emptying the method registry so all commands execute natively without mongosh fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove empty method registry All M3 methods are now implemented natively, so the method registry that tracked planned-but-unimplemented methods is no longer needed. - Delete internal/translator/method_registry.go and method_registry.go - Remove public IsPlannedMethod/MethodRegistryStats APIs from errors.go - Simplify handleUnsupportedMethod to always return UnsupportedOperationError - Remove obsolete TestPlannedOperation and TestMethodRegistryStats tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add strict type validation for createIndexes and db.version() Address PR review feedback: - Validate 'key' field is a non-empty bson.D in extractCreateIndexesArgs - Return errors on type mismatches for createIndexes spec fields (key, name, unique, sparse, expireAfterSeconds) instead of silently ignoring - Return error when version field is missing from buildInfo result in executeDbVersion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: lint issues and DocumentDB test compatibility - Fix ineffectual assignment lint error in extractCreateIndexesArgs - Remove redundant nil check before len() (staticcheck S1009) - Skip TestServerStatus on DocumentDB (serverStatus unsupported) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0425183 commit d2ea344

File tree

12 files changed

+780
-133
lines changed

12 files changed

+780
-133
lines changed

admin_test.go

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,353 @@ func TestCreateIndexWithBackgroundOption(t *testing.T) {
477477
require.Contains(t, row, "name")
478478
})
479479
}
480+
481+
func TestCreateIndexes(t *testing.T) {
482+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
483+
dbName := fmt.Sprintf("testdb_create_idxs_%s", db.Name)
484+
defer testutil.CleanupDatabase(t, db.Client, dbName)
485+
486+
ctx := context.Background()
487+
488+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice", "email": "alice@example.com", "age": 30})
489+
require.NoError(t, err)
490+
491+
gc := gomongo.NewClient(db.Client)
492+
493+
result, err := gc.Execute(ctx, dbName, `db.users.createIndexes([{ key: { name: 1 }, name: "name_idx" }, { key: { email: 1 }, name: "email_idx" }])`)
494+
require.NoError(t, err)
495+
require.NotNil(t, result)
496+
require.Equal(t, 2, len(result.Value))
497+
498+
// Values should be index name strings
499+
name0, ok := result.Value[0].(string)
500+
require.True(t, ok)
501+
require.Equal(t, "name_idx", name0)
502+
name1, ok := result.Value[1].(string)
503+
require.True(t, ok)
504+
require.Equal(t, "email_idx", name1)
505+
506+
// Verify indexes exist
507+
idxResult, err := gc.Execute(ctx, dbName, `db.users.getIndexes()`)
508+
require.NoError(t, err)
509+
require.Equal(t, 3, len(idxResult.Value)) // _id + name_idx + email_idx
510+
})
511+
}
512+
513+
func TestDbStats(t *testing.T) {
514+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
515+
dbName := fmt.Sprintf("testdb_db_stats_%s", db.Name)
516+
defer testutil.CleanupDatabase(t, db.Client, dbName)
517+
518+
ctx := context.Background()
519+
520+
// Create some data
521+
_, err := db.Client.Database(dbName).Collection("test").InsertOne(ctx, bson.M{"x": 1})
522+
require.NoError(t, err)
523+
524+
gc := gomongo.NewClient(db.Client)
525+
526+
result, err := gc.Execute(ctx, dbName, `db.stats()`)
527+
require.NoError(t, err)
528+
require.NotNil(t, result)
529+
require.Equal(t, 1, len(result.Value))
530+
531+
row := valueToJSON(result.Value[0])
532+
require.Contains(t, row, `"db"`)
533+
require.Contains(t, row, `"collections"`)
534+
})
535+
}
536+
537+
func TestCollectionStats(t *testing.T) {
538+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
539+
dbName := fmt.Sprintf("testdb_coll_stats_%s", db.Name)
540+
defer testutil.CleanupDatabase(t, db.Client, dbName)
541+
542+
ctx := context.Background()
543+
544+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
545+
require.NoError(t, err)
546+
547+
gc := gomongo.NewClient(db.Client)
548+
549+
result, err := gc.Execute(ctx, dbName, `db.users.stats()`)
550+
require.NoError(t, err)
551+
require.NotNil(t, result)
552+
require.Equal(t, 1, len(result.Value))
553+
554+
row := valueToJSON(result.Value[0])
555+
require.Contains(t, row, `"ns"`)
556+
require.Contains(t, row, `"count"`)
557+
})
558+
}
559+
560+
func TestServerStatus(t *testing.T) {
561+
// serverStatus is not supported on DocumentDB
562+
testutil.RunOnMongoDBOnly(t, func(t *testing.T, db testutil.TestDB) {
563+
dbName := fmt.Sprintf("testdb_server_status_%s", db.Name)
564+
defer testutil.CleanupDatabase(t, db.Client, dbName)
565+
566+
ctx := context.Background()
567+
gc := gomongo.NewClient(db.Client)
568+
569+
result, err := gc.Execute(ctx, dbName, `db.serverStatus()`)
570+
require.NoError(t, err)
571+
require.NotNil(t, result)
572+
require.Equal(t, 1, len(result.Value))
573+
574+
row := valueToJSON(result.Value[0])
575+
require.Contains(t, row, `"ok"`)
576+
})
577+
}
578+
579+
func TestServerBuildInfo(t *testing.T) {
580+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
581+
dbName := fmt.Sprintf("testdb_build_info_%s", db.Name)
582+
defer testutil.CleanupDatabase(t, db.Client, dbName)
583+
584+
ctx := context.Background()
585+
gc := gomongo.NewClient(db.Client)
586+
587+
result, err := gc.Execute(ctx, dbName, `db.serverBuildInfo()`)
588+
require.NoError(t, err)
589+
require.NotNil(t, result)
590+
require.Equal(t, 1, len(result.Value))
591+
592+
row := valueToJSON(result.Value[0])
593+
require.Contains(t, row, `"version"`)
594+
})
595+
}
596+
597+
func TestDbVersion(t *testing.T) {
598+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
599+
dbName := fmt.Sprintf("testdb_db_version_%s", db.Name)
600+
defer testutil.CleanupDatabase(t, db.Client, dbName)
601+
602+
ctx := context.Background()
603+
gc := gomongo.NewClient(db.Client)
604+
605+
result, err := gc.Execute(ctx, dbName, `db.version()`)
606+
require.NoError(t, err)
607+
require.NotNil(t, result)
608+
require.Equal(t, 1, len(result.Value))
609+
610+
version, ok := result.Value[0].(string)
611+
require.True(t, ok)
612+
require.NotEmpty(t, version)
613+
// Version should look like a semver (e.g., "4.4.0", "8.0.0")
614+
require.True(t, strings.Contains(version, "."), "version should contain dots: %s", version)
615+
})
616+
}
617+
618+
func TestHostInfo(t *testing.T) {
619+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
620+
dbName := fmt.Sprintf("testdb_host_info_%s", db.Name)
621+
defer testutil.CleanupDatabase(t, db.Client, dbName)
622+
623+
ctx := context.Background()
624+
gc := gomongo.NewClient(db.Client)
625+
626+
result, err := gc.Execute(ctx, dbName, `db.hostInfo()`)
627+
require.NoError(t, err)
628+
require.NotNil(t, result)
629+
require.Equal(t, 1, len(result.Value))
630+
631+
row := valueToJSON(result.Value[0])
632+
require.Contains(t, row, `"ok"`)
633+
})
634+
}
635+
636+
func TestListCommands(t *testing.T) {
637+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
638+
dbName := fmt.Sprintf("testdb_list_cmds_%s", db.Name)
639+
defer testutil.CleanupDatabase(t, db.Client, dbName)
640+
641+
ctx := context.Background()
642+
gc := gomongo.NewClient(db.Client)
643+
644+
result, err := gc.Execute(ctx, dbName, `db.listCommands()`)
645+
require.NoError(t, err)
646+
require.NotNil(t, result)
647+
require.Equal(t, 1, len(result.Value))
648+
649+
row := valueToJSON(result.Value[0])
650+
require.Contains(t, row, `"ok"`)
651+
require.Contains(t, row, `"commands"`)
652+
})
653+
}
654+
655+
func TestDataSize(t *testing.T) {
656+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
657+
dbName := fmt.Sprintf("testdb_data_size_%s", db.Name)
658+
defer testutil.CleanupDatabase(t, db.Client, dbName)
659+
660+
ctx := context.Background()
661+
662+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
663+
require.NoError(t, err)
664+
665+
gc := gomongo.NewClient(db.Client)
666+
667+
result, err := gc.Execute(ctx, dbName, `db.users.dataSize()`)
668+
require.NoError(t, err)
669+
require.NotNil(t, result)
670+
require.Equal(t, 1, len(result.Value))
671+
// dataSize returns a numeric value (int32 or int64)
672+
require.NotNil(t, result.Value[0])
673+
})
674+
}
675+
676+
func TestStorageSize(t *testing.T) {
677+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
678+
dbName := fmt.Sprintf("testdb_storage_size_%s", db.Name)
679+
defer testutil.CleanupDatabase(t, db.Client, dbName)
680+
681+
ctx := context.Background()
682+
683+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
684+
require.NoError(t, err)
685+
686+
gc := gomongo.NewClient(db.Client)
687+
688+
result, err := gc.Execute(ctx, dbName, `db.users.storageSize()`)
689+
require.NoError(t, err)
690+
require.NotNil(t, result)
691+
require.Equal(t, 1, len(result.Value))
692+
require.NotNil(t, result.Value[0])
693+
})
694+
}
695+
696+
func TestTotalIndexSize(t *testing.T) {
697+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
698+
dbName := fmt.Sprintf("testdb_total_idx_size_%s", db.Name)
699+
defer testutil.CleanupDatabase(t, db.Client, dbName)
700+
701+
ctx := context.Background()
702+
703+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
704+
require.NoError(t, err)
705+
706+
gc := gomongo.NewClient(db.Client)
707+
708+
result, err := gc.Execute(ctx, dbName, `db.users.totalIndexSize()`)
709+
require.NoError(t, err)
710+
require.NotNil(t, result)
711+
require.Equal(t, 1, len(result.Value))
712+
require.NotNil(t, result.Value[0])
713+
})
714+
}
715+
716+
func TestTotalSize(t *testing.T) {
717+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
718+
dbName := fmt.Sprintf("testdb_total_size_%s", db.Name)
719+
defer testutil.CleanupDatabase(t, db.Client, dbName)
720+
721+
ctx := context.Background()
722+
723+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
724+
require.NoError(t, err)
725+
726+
gc := gomongo.NewClient(db.Client)
727+
728+
result, err := gc.Execute(ctx, dbName, `db.users.totalSize()`)
729+
require.NoError(t, err)
730+
require.NotNil(t, result)
731+
require.Equal(t, 1, len(result.Value))
732+
// totalSize is int64 (storageSize + totalIndexSize)
733+
_, ok := result.Value[0].(int64)
734+
require.True(t, ok, "expected int64, got %T", result.Value[0])
735+
})
736+
}
737+
738+
func TestIsCapped(t *testing.T) {
739+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
740+
dbName := fmt.Sprintf("testdb_is_capped_%s", db.Name)
741+
defer testutil.CleanupDatabase(t, db.Client, dbName)
742+
743+
ctx := context.Background()
744+
745+
// Regular collection should not be capped
746+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
747+
require.NoError(t, err)
748+
749+
gc := gomongo.NewClient(db.Client)
750+
751+
result, err := gc.Execute(ctx, dbName, `db.users.isCapped()`)
752+
require.NoError(t, err)
753+
require.NotNil(t, result)
754+
require.Equal(t, 1, len(result.Value))
755+
756+
capped, ok := result.Value[0].(bool)
757+
require.True(t, ok)
758+
require.False(t, capped)
759+
})
760+
}
761+
762+
func TestIsCappedTrue(t *testing.T) {
763+
testutil.RunOnMongoDBOnly(t, func(t *testing.T, db testutil.TestDB) {
764+
dbName := fmt.Sprintf("testdb_is_capped_true_%s", db.Name)
765+
defer testutil.CleanupDatabase(t, db.Client, dbName)
766+
767+
ctx := context.Background()
768+
gc := gomongo.NewClient(db.Client)
769+
770+
// Create a capped collection
771+
_, err := gc.Execute(ctx, dbName, `db.createCollection("capped_coll", { capped: true, size: 1048576 })`)
772+
require.NoError(t, err)
773+
774+
result, err := gc.Execute(ctx, dbName, `db.capped_coll.isCapped()`)
775+
require.NoError(t, err)
776+
require.NotNil(t, result)
777+
require.Equal(t, 1, len(result.Value))
778+
779+
capped, ok := result.Value[0].(bool)
780+
require.True(t, ok)
781+
require.True(t, capped)
782+
})
783+
}
784+
785+
func TestValidate(t *testing.T) {
786+
testutil.RunOnAllDBs(t, func(t *testing.T, db testutil.TestDB) {
787+
dbName := fmt.Sprintf("testdb_validate_%s", db.Name)
788+
defer testutil.CleanupDatabase(t, db.Client, dbName)
789+
790+
ctx := context.Background()
791+
792+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
793+
require.NoError(t, err)
794+
795+
gc := gomongo.NewClient(db.Client)
796+
797+
result, err := gc.Execute(ctx, dbName, `db.users.validate()`)
798+
require.NoError(t, err)
799+
require.NotNil(t, result)
800+
require.Equal(t, 1, len(result.Value))
801+
802+
row := valueToJSON(result.Value[0])
803+
require.Contains(t, row, `"ok"`)
804+
require.Contains(t, row, `"ns"`)
805+
})
806+
}
807+
808+
func TestLatencyStats(t *testing.T) {
809+
// latencyStats uses $collStats aggregation which may not be available on all platforms
810+
testutil.RunOnMongoDBOnly(t, func(t *testing.T, db testutil.TestDB) {
811+
dbName := fmt.Sprintf("testdb_latency_%s", db.Name)
812+
defer testutil.CleanupDatabase(t, db.Client, dbName)
813+
814+
ctx := context.Background()
815+
816+
_, err := db.Client.Database(dbName).Collection("users").InsertOne(ctx, bson.M{"name": "alice"})
817+
require.NoError(t, err)
818+
819+
gc := gomongo.NewClient(db.Client)
820+
821+
result, err := gc.Execute(ctx, dbName, `db.users.latencyStats()`)
822+
require.NoError(t, err)
823+
require.NotNil(t, result)
824+
require.GreaterOrEqual(t, len(result.Value), 1)
825+
826+
row := valueToJSON(result.Value[0])
827+
require.Contains(t, row, `"latencyStats"`)
828+
})
829+
}

client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,15 @@ func NewClient(client *mongo.Client) *Client {
2828
// - OpShowDatabases, OpShowCollections, OpGetCollectionNames: each element is string
2929
// - OpInsertOne, OpInsertMany, OpUpdateOne, OpUpdateMany, OpReplaceOne, OpDeleteOne, OpDeleteMany: single bson.D with operation result
3030
// - OpCreateIndex: single element of string (index name)
31+
// - OpCreateIndexes: each element is string (index name)
3132
// - OpDropIndex, OpDropIndexes, OpCreateCollection, OpDropDatabase, OpRenameCollection: single bson.D with {ok: 1}
3233
// - OpDrop: single element of bool (true)
34+
// - OpDbStats, OpCollectionStats, OpServerStatus, OpServerBuildInfo, OpHostInfo, OpListCommands, OpValidate: single bson.D (command result)
35+
// - OpDbVersion: single element of string (version)
36+
// - OpDataSize, OpStorageSize, OpTotalIndexSize: single numeric value from collStats
37+
// - OpTotalSize: single int64 (storageSize + totalIndexSize)
38+
// - OpIsCapped: single element of bool
39+
// - OpLatencyStats: each element is bson.D (aggregation result)
3340
type Result struct {
3441
Operation types.OperationType
3542
Value []any

0 commit comments

Comments
 (0)