From 5583314d0aa4c27639f9501df06ea90ed719769a Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 1 May 2026 17:01:42 -0500 Subject: [PATCH] up --- go.mod | 7 + go.sum | 15 ++ pkg/services/query_runner_service.go | 387 ++++++++++++++++++++++++++- 3 files changed, 408 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8f5f47d..63114a9 100755 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -106,6 +107,7 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oschwald/maxminddb-golang v1.13.1 // indirect @@ -122,7 +124,12 @@ require ( github.com/valyala/fasthttp v1.56.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect diff --git a/go.sum b/go.sum index 58a6cda..321df45 100755 --- a/go.sum +++ b/go.sum @@ -287,6 +287,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= @@ -512,6 +514,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -671,11 +675,19 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -688,6 +700,8 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -966,6 +980,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/pkg/services/query_runner_service.go b/pkg/services/query_runner_service.go index c5d3097..bf1172c 100644 --- a/pkg/services/query_runner_service.go +++ b/pkg/services/query_runner_service.go @@ -3,7 +3,9 @@ package services import ( "context" "database/sql" + "encoding/json" "fmt" + "regexp" "strings" "time" @@ -11,6 +13,9 @@ import ( _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" _ "github.com/microsoft/go-mssqldb" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) @@ -76,6 +81,9 @@ func openDynamicDB(c models.ConxDb) (*sql.DB, error) { // ExecuteSQL ejecuta SQL arbitrario contra la conexión y devuelve QueryResult. // También guarda en query_history. func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult { + if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) { + return mongoExecuteSQL(conx, database, sqlText) + } start := time.Now() db, err := openDynamicDB(conx) @@ -161,13 +169,16 @@ func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult { // ListDatabases devuelve la lista de bases de datos del servidor. func ListDatabases(conx models.ConxDb) ([]string, error) { + driver := strings.ToLower(conx.TipoDb.Nombre) + if isMongoDriver(driver) { + return mongoListDatabases(conx) + } db, err := openDynamicDB(conx) if err != nil { return nil, err } defer db.Close() - driver := strings.ToLower(conx.TipoDb.Nombre) var query string switch { case strings.Contains(driver, "postgres"): @@ -200,6 +211,9 @@ func ListDatabases(conx models.ConxDb) ([]string, error) { // ListTables devuelve las tablas de una base de datos. func ListTables(conx models.ConxDb, database string) ([]string, error) { driver := strings.ToLower(conx.TipoDb.Nombre) + if isMongoDriver(driver) { + return mongoListCollections(conx, database) + } var db *sql.DB var err error @@ -246,6 +260,9 @@ func ListTables(conx models.ConxDb, database string) ([]string, error) { // TestConnection verifica si la conexión es válida. func TestDBConnection(conx models.ConxDb) error { + if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) { + return mongoTestConnection(conx) + } db, err := openDynamicDB(conx) if err != nil { return err @@ -315,3 +332,371 @@ func saveHistory(conxID uint, sqlText, status, errMsg string, rows, durationMs i ExecutedAt: time.Now(), }) } + +// ── MongoDB ─────────────────────────────────────────────────────────────────── + +func isMongoDriver(driver string) bool { + return strings.Contains(driver, "mongo") +} + +func mongoURI(c models.ConxDb) string { + host := c.Servidor.IpServidor + port := c.Puerto + user := c.Usuario + pass := c.Password + if user != "" && pass != "" { + return fmt.Sprintf("mongodb://%s:%s@%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", user, pass, host, port) + } + return fmt.Sprintf("mongodb://%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", host, port) +} + +func mongoConnect(c models.ConxDb) (*mongo.Client, error) { + ctx, cancel := context.WithTimeout(context.Background(), connTimeout) + defer cancel() + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI(c))) + if err != nil { + return nil, err + } + if err := client.Ping(ctx, nil); err != nil { + client.Disconnect(context.Background()) //nolint + return nil, fmt.Errorf("no se pudo conectar a MongoDB: %w", err) + } + return client, nil +} + +func mongoTestConnection(c models.ConxDb) error { + client, err := mongoConnect(c) + if err != nil { + return err + } + client.Disconnect(context.Background()) //nolint + return nil +} + +func mongoListDatabases(c models.ConxDb) ([]string, error) { + client, err := mongoConnect(c) + if err != nil { + return nil, err + } + defer client.Disconnect(context.Background()) //nolint + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + return client.ListDatabaseNames(ctx, bson.M{}) +} + +func mongoListCollections(c models.ConxDb, database string) ([]string, error) { + client, err := mongoConnect(c) + if err != nil { + return nil, err + } + defer client.Disconnect(context.Background()) //nolint + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + return client.Database(database).ListCollectionNames(ctx, bson.M{}) +} + +var mongoQueryRe = regexp.MustCompile(`(?s)^db\.(\w[\w\d_]*)\.([\w]+)\((.*)\)\s*$`) + +func mongoExecuteSQL(conx models.ConxDb, database, queryText string) QueryResult { + start := time.Now() + client, err := mongoConnect(conx) + if err != nil { + saveHistory(conx.ID, queryText, "error", err.Error(), 0, time.Since(start).Milliseconds()) + return QueryResult{Error: err.Error()} + } + defer client.Disconnect(context.Background()) //nolint + db := client.Database(database) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + result, err := mongoRunQuery(ctx, db, strings.TrimSpace(queryText)) + elapsed := time.Since(start).Milliseconds() + if err != nil { + saveHistory(conx.ID, queryText, "error", err.Error(), 0, elapsed) + return QueryResult{Error: err.Error()} + } + result.DurationMs = elapsed + saveHistory(conx.ID, queryText, "ok", "", int64(result.RowCount)+result.AffectedRows, elapsed) + return *result +} + +func mongoRunQuery(ctx context.Context, db *mongo.Database, query string) (*QueryResult, error) { + m := mongoQueryRe.FindStringSubmatch(query) + if m == nil { + return nil, fmt.Errorf("sintaxis no reconocida. Use: db.coleccion.metodo({...})") + } + collName, method, argsRaw := m[1], strings.ToLower(m[2]), strings.TrimSpace(m[3]) + if collName == "runCommand" { + return mongoRunCommand(ctx, db, argsRaw) + } + coll := db.Collection(collName) + args := splitTopLevelArgs(argsRaw) + switch method { + case "find": + return mongoFind(ctx, coll, args, false) + case "findone": + return mongoFind(ctx, coll, args, true) + case "insertone": + return mongoInsertOne(ctx, coll, args) + case "insertmany": + return mongoInsertMany(ctx, coll, args) + case "updateone": + return mongoUpdate(ctx, coll, args, false) + case "updatemany": + return mongoUpdate(ctx, coll, args, true) + case "deleteone": + return mongoDelete(ctx, coll, args, false) + case "deletemany": + return mongoDelete(ctx, coll, args, true) + case "countdocuments": + return mongoCount(ctx, coll, args) + case "aggregate": + return mongoAggregate(ctx, coll, argsRaw) + case "drop": + if err := coll.Drop(ctx); err != nil { + return nil, err + } + return &QueryResult{IsSelect: false}, nil + default: + return nil, fmt.Errorf("método MongoDB no soportado: %s", m[2]) + } +} + +func mongoFind(ctx context.Context, coll *mongo.Collection, args []string, one bool) (*QueryResult, error) { + var filter bson.M + if len(args) > 0 && args[0] != "" { + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil { + return nil, fmt.Errorf("filtro inválido: %w", err) + } + } else { + filter = bson.M{} + } + var docs []bson.M + if one { + var doc bson.M + if err := coll.FindOne(ctx, filter).Decode(&doc); err != nil { + if err == mongo.ErrNoDocuments { + return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}}, nil + } + return nil, err + } + docs = []bson.M{doc} + } else { + cursor, err := coll.Find(ctx, filter) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + if err := cursor.All(ctx, &docs); err != nil { + return nil, err + } + } + return bsonDocsToResult(docs), nil +} + +func mongoInsertOne(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) { + if len(args) == 0 || args[0] == "" { + return nil, fmt.Errorf("insertOne requiere un documento") + } + var doc bson.M + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &doc); err != nil { + return nil, fmt.Errorf("documento inválido: %w", err) + } + res, err := coll.InsertOne(ctx, doc) + if err != nil { + return nil, err + } + return &QueryResult{ + IsSelect: true, AffectedRows: 1, + Columns: []string{"insertedId"}, + Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}}, + RowCount: 1, + }, nil +} + +func mongoInsertMany(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) { + if len(args) == 0 || args[0] == "" { + return nil, fmt.Errorf("insertMany requiere un array de documentos") + } + var arr bson.A + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &arr); err != nil { + return nil, fmt.Errorf("documentos inválidos: %w", err) + } + docs := make([]interface{}, len(arr)) + copy(docs, arr) + res, err := coll.InsertMany(ctx, docs) + if err != nil { + return nil, err + } + return &QueryResult{IsSelect: false, AffectedRows: int64(len(res.InsertedIDs))}, nil +} + +func mongoUpdate(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) { + if len(args) < 2 { + return nil, fmt.Errorf("update requiere filtro y documento de actualización") + } + var filter, update bson.M + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil { + return nil, fmt.Errorf("filtro inválido: %w", err) + } + if err := bson.UnmarshalExtJSON([]byte(args[1]), true, &update); err != nil { + return nil, fmt.Errorf("update inválido: %w", err) + } + var affected int64 + if many { + res, err := coll.UpdateMany(ctx, filter, update) + if err != nil { + return nil, err + } + affected = res.ModifiedCount + } else { + res, err := coll.UpdateOne(ctx, filter, update) + if err != nil { + return nil, err + } + affected = res.ModifiedCount + } + return &QueryResult{IsSelect: false, AffectedRows: affected}, nil +} + +func mongoDelete(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) { + var filter bson.M + if len(args) > 0 && args[0] != "" { + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil { + return nil, fmt.Errorf("filtro inválido: %w", err) + } + } else { + filter = bson.M{} + } + var deleted int64 + if many { + res, err := coll.DeleteMany(ctx, filter) + if err != nil { + return nil, err + } + deleted = res.DeletedCount + } else { + res, err := coll.DeleteOne(ctx, filter) + if err != nil { + return nil, err + } + deleted = res.DeletedCount + } + return &QueryResult{IsSelect: false, AffectedRows: deleted}, nil +} + +func mongoCount(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) { + var filter bson.M + if len(args) > 0 && args[0] != "" { + if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil { + return nil, fmt.Errorf("filtro inválido: %w", err) + } + } else { + filter = bson.M{} + } + count, err := coll.CountDocuments(ctx, filter) + if err != nil { + return nil, err + } + return &QueryResult{ + IsSelect: true, + Columns: []string{"count"}, + Rows: []map[string]any{{"count": count}}, + RowCount: 1, + }, nil +} + +func mongoAggregate(ctx context.Context, coll *mongo.Collection, pipelineStr string) (*QueryResult, error) { + var pipeline bson.A + if err := bson.UnmarshalExtJSON([]byte(pipelineStr), true, &pipeline); err != nil { + return nil, fmt.Errorf("pipeline inválido: %w", err) + } + cursor, err := coll.Aggregate(ctx, pipeline) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + var docs []bson.M + if err := cursor.All(ctx, &docs); err != nil { + return nil, err + } + return bsonDocsToResult(docs), nil +} + +func mongoRunCommand(ctx context.Context, db *mongo.Database, cmdStr string) (*QueryResult, error) { + var cmd bson.D + if err := bson.UnmarshalExtJSON([]byte(cmdStr), true, &cmd); err != nil { + return nil, fmt.Errorf("comando inválido: %w", err) + } + var result bson.M + if err := db.RunCommand(ctx, cmd).Decode(&result); err != nil { + return nil, err + } + return bsonDocsToResult([]bson.M{result}), nil +} + +func bsonDocsToResult(docs []bson.M) *QueryResult { + if len(docs) == 0 { + return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}} + } + keyIdx := make(map[string]int) + for _, doc := range docs { + for k := range doc { + if _, seen := keyIdx[k]; !seen { + keyIdx[k] = len(keyIdx) + } + } + } + cols := make([]string, len(keyIdx)) + for k, i := range keyIdx { + cols[i] = k + } + rows := make([]map[string]any, len(docs)) + for i, doc := range docs { + row := make(map[string]any, len(doc)) + for k, v := range doc { + switch v.(type) { + case bson.M, bson.A, bson.D: + b, _ := json.Marshal(v) + row[k] = string(b) + default: + row[k] = fmt.Sprintf("%v", v) + } + } + rows[i] = row + } + return &QueryResult{IsSelect: true, Columns: cols, Rows: rows, RowCount: len(rows)} +} + +func splitTopLevelArgs(s string) []string { + var args []string + depth := 0 + inStr := false + start := 0 + s = strings.TrimSpace(s) + for i := 0; i < len(s); i++ { + ch := s[i] + switch ch { + case '"': + if i == 0 || s[i-1] != '\\' { + inStr = !inStr + } + case '{', '[', '(': + if !inStr { + depth++ + } + case '}', ']', ')': + if !inStr { + depth-- + } + case ',': + if !inStr && depth == 0 { + args = append(args, strings.TrimSpace(s[start:i])) + start = i + 1 + } + } + } + if last := strings.TrimSpace(s[start:]); last != "" { + args = append(args, last) + } + return args +}