This commit is contained in:
Lizandro Guarnizo
2026-05-01 17:01:42 -05:00
parent f8e4ee5b05
commit 5583314d0a
3 changed files with 408 additions and 1 deletions
+7
View File
@@ -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
+15
View File
@@ -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=
+386 -1
View File
@@ -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
}