Skip to content

Commit

Permalink
add explain() support
Browse files Browse the repository at this point in the history
folowing queries are now valid:

db.collection.find().explain()
db.collection.explain().find()
db.collections.explain("executionStats").find()

closes #28
  • Loading branch information
feliixx committed Jan 12, 2021
1 parent 95673f6 commit f7600ac
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 72 deletions.
43 changes: 40 additions & 3 deletions javascript_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ func TestJavascriptIndentRoundTrip(t *testing.T) {
indent: `db.collection.find()/** comment with no line return*/
`,
},
{
name: "aggregate with explain",
input: `db.collection.find().explain(
)`,
compact: `db.collection.find().explain()`,
indent: `db.collection.find().explain()`,
},
}

buffer := loadPlaygroundJs(t)
Expand Down Expand Up @@ -469,6 +477,20 @@ func TestCompactAndRemoveComment(t *testing.T) {
*/`,
expected: `db.collection.find({k:1})`,
},
{
name: "explain",
input: `
db.collection.find().explain( )`,
expected: `db.collection.find().explain()`,
},
{
name: "explain before aggregate",
input: `
db.collection.explain(
"queryPlanner"
).aggregate([])`,
expected: `db.collection.explain("queryPlanner").aggregate([])`,
},
}

buffer := loadPlaygroundJs(t)
Expand Down Expand Up @@ -507,7 +529,7 @@ func TestCompactAndRemoveComment(t *testing.T) {

}

func TestFormatConfig(t *testing.T) {
func TestValidConfig(t *testing.T) {

t.Parallel()

Expand Down Expand Up @@ -590,7 +612,7 @@ func TestFormatConfig(t *testing.T) {

}

func TestFormatQuery(t *testing.T) {
func TestValidQuery(t *testing.T) {

t.Parallel()

Expand Down Expand Up @@ -660,7 +682,7 @@ func TestFormatQuery(t *testing.T) {
},
{
name: `chained non-empty method`,
input: `db.collection.aggregate([{"$match": { "_id": ObjectId("5a934e000102030405000000")}}]).explain("executionTimeMillis")`,
input: `db.collection.aggregate([{"$match": { "_id": ObjectId("5a934e000102030405000000")}}]).pretty()`,
valid: false,
},
{
Expand Down Expand Up @@ -693,6 +715,21 @@ db.collection.aggregate([{"$match": { "_id": ObjectId("5a934e000102030405000000"
input: `db.collection.update({"k":1},{"$set":{"a":true}})`,
valid: true,
},
{
name: `explain`,
input: `db.collection.find({"k":1}).explain()`,
valid: true,
},
{
name: `explain with option`,
input: `db.collection.find({"k":1}).explain("executionStats")`,
valid: true,
},
{
name: `explain before find`,
input: `db.collection.explain("queryPlanner").find({"k":1})`,
valid: true,
},
}

buffer := loadPlaygroundJs(t)
Expand Down
11 changes: 7 additions & 4 deletions playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="description" content="Mongo playground: a simple sandbox to test and share MongoDB queries online">
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link href="/static/playground-min-10.css" rel="stylesheet" type="text/css">
<script src="/static/playground-min-10.js" type="text/javascript"></script>
<script src="/static/playground-min-11.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.10/ace.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.10/ext-language_tools.js"
type="text/javascript"></script>
Expand Down Expand Up @@ -122,6 +122,7 @@
function setTemplate(index) {
configEditor.setValue(indent(templates[index].config), 1)
queryEditor.setValue(indent(templates[index].query), 1)
document.getElementById(templates[index].mode).checked = true
}

function showDoc(doShow) {
Expand Down Expand Up @@ -165,7 +166,7 @@

hasChangedSinceLastRun = false
var response = r.responseText
if (response.startsWith("[")) {
if (response.startsWith("[") || response.startsWith("{")) {
showResult(response, true)
} else if (response === "no document found") {
showResult(response, false)
Expand Down Expand Up @@ -297,11 +298,13 @@
<option value=1>bson multiple collections</option>
<option value=2>mgodatagen</option>
<option value=3>update</option>
<option value=4>indexe</option>
<option value=5>explain</option>
</select>
<label class="bold">Mode:</label>
<input type="radio" name="mode" value="bson" onchange="changeFunc()" {{if eq .Mode 1 }} checked {{end}} />
<input id="bson" type="radio" name="mode" value="bson" onchange="changeFunc()" {{if eq .Mode 1 }} checked {{end}} />
<label for="bson">bson</label>
<input type="radio" name="mode" value="mgodatagen" onchange="changeFunc()" {{if eq .Mode 0 }} checked
<input id="mgodatagen" type="radio" name="mode" value="mgodatagen" onchange="changeFunc()" {{if eq .Mode 0 }} checked
{{end}} />
<label for="mgodatagen">mgodatagen</label>
<input type="button" value="docs" onclick="showDoc(true)">
Expand Down
111 changes: 86 additions & 25 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,9 @@ db = {
errPlaygroundToBig = "playground is too big"
noDocFound = "no document found"

findMethod = "find"
aggregateMethod = "aggregate"
updateMethod = "update"
getIndexesMethod = "getIndexes"
findMethod = "find"
aggregateMethod = "aggregate"
updateMethod = "update"
)

// run a query and return the results as plain text.
Expand Down Expand Up @@ -100,7 +99,7 @@ func (s *server) runHandler(w http.ResponseWriter, r *http.Request) {

func (s *server) run(p *page) ([]byte, error) {

collectionName, method, stages, err := parseQuery(p.Query)
collectionName, method, stages, explainMode, err := parseQuery(p.Query)
if err != nil {
return nil, fmt.Errorf("error in query:\n %v", err)
}
Expand All @@ -121,7 +120,7 @@ func (s *server) run(p *page) ([]byte, error) {
if !dbInfos.hasCollection(collectionName) {
return nil, fmt.Errorf(`collection "%s" doesn't exist`, collectionName)
}
return runQuery(db.Collection(collectionName), method, stages)
return runQuery(db.Collection(collectionName), method, stages, explainMode)
}

func (s *server) createDatabase(db *mongo.Database, mode byte, config []byte, forceCreate bool) (dbInfo dbMetaInfo, err error) {
Expand Down Expand Up @@ -352,11 +351,32 @@ func seededObjectID(n int32) primitive.ObjectID {
//
// input is filtered from front-end side, but this should
// not panic on pathological/malformatted input
func parseQuery(query []byte) (collectionName, method string, stages []interface{}, err error) {
func parseQuery(query []byte) (collectionName, method string, stages []interface{}, explainMode string, err error) {

startExplain := bytes.Index(query, []byte(".explain("))
if startExplain != -1 {
endExplain := bytes.Index(query[startExplain:], []byte(")"))
if endExplain != -1 {
endExplain += startExplain
explainMode = string(query[startExplain+9 : endExplain])

if endExplain+1 == len(query) {
query = query[:startExplain]
} else {
query = append(query[:startExplain], query[endExplain+1:]...)
}

if explainMode == "" {
explainMode = "queryPlanner"
} else {
explainMode = explainMode[1 : len(explainMode)-1]
}
}
}

p := bytes.SplitN(query, []byte{'.'}, 3)
if len(p) != 3 {
return "", "", nil, errors.New(errInvalidQuery)
return "", "", nil, "", errors.New(errInvalidQuery)
}

collectionName = string(p[1])
Expand All @@ -366,17 +386,17 @@ func parseQuery(query []byte) (collectionName, method string, stages []interface
start, end := bytes.IndexByte(queryBytes, '('), bytes.LastIndexByte(queryBytes, ')')

if start == -1 || end == -1 {
return "", "", nil, errors.New(errInvalidQuery)
return "", "", nil, "", errors.New(errInvalidQuery)
}

method = string(queryBytes[:start])

stages, err = unmarshalStages(queryBytes[start+1 : end])
if err != nil {
return "", "", nil, fmt.Errorf("fail to parse content of query: %v", err)
return "", "", nil, "", fmt.Errorf("fail to parse content of query: %v", err)
}

return collectionName, method, stages, nil
return collectionName, method, stages, explainMode, nil
}

// most of the time, each stage is a bson.M document.
Expand Down Expand Up @@ -408,49 +428,90 @@ func unmarshalStages(queryBytes []byte) (stages []interface{}, err error) {
return stages, err
}

func runQuery(collection *mongo.Collection, method string, stages []interface{}) ([]byte, error) {
func runQuery(collection *mongo.Collection, method string, stages []interface{}, explainMode string) ([]byte, error) {

var docs []bson.M
var err error
var cursor *mongo.Cursor
var cmd bson.D

switch method {
case aggregateMethod:
cursor, err = collection.Aggregate(context.Background(), stages, options.Aggregate().SetMaxTime(maxQueryTime))

cmd = bson.D{
{Key: aggregateMethod, Value: collection.Name()},
{Key: "pipeline", Value: stages},
{Key: "cursor", Value: bson.M{}},
}

case findMethod:

for len(stages) < 2 {
stages = append(stages, bson.M{})
}
cursor, err = collection.Find(context.Background(), stages[0], options.Find().SetProjection(stages[1]).SetMaxTime(maxQueryTime))

cmd = bson.D{
{Key: findMethod, Value: collection.Name()},
{Key: "filter", Value: stages[0]},
{Key: "projection", Value: stages[1]},
}

case updateMethod:

for len(stages) < 3 {
stages = append(stages, bson.M{})
}

var err error
multi, opts := parseUpdateOpts(stages[2])
if multi {
_, err = collection.UpdateMany(context.Background(), stages[0], stages[1], opts)
} else {
_, err = collection.UpdateOne(context.Background(), stages[0], stages[1], opts)
}

if err != nil {
return nil, fmt.Errorf("fail to run update: %v", err)
}
cursor, err = collection.Find(context.Background(), bson.M{})
case getIndexesMethod:
cursor, err = collection.Indexes().List(context.Background())

cmd = bson.D{
{Key: findMethod, Value: collection.Name()},
{Key: "filter", Value: bson.M{}},
}

default:
err = fmt.Errorf("invalid method: %s", method)
return nil, fmt.Errorf("invalid method: '%s'", method)
}
if err != nil {
return nil, fmt.Errorf("query failed: %v", err)

// make sure that all types of queries have a timeout,
// even in explain mode
cmd = append(cmd, bson.E{Key: "maxTimeMS", Value: maxQueryTime.Milliseconds()})

if explainMode != "" {
cmd = bson.D{
{Key: "explain", Value: cmd},
{Key: "verbosity", Value: explainMode},
}
}

res := collection.Database().RunCommand(context.Background(), cmd)
if res.Err() != nil {
return nil, fmt.Errorf("query failed: %v", res.Err())
}

if err = cursor.All(context.Background(), &docs); err != nil {
var cursorDoc bson.M
if err := res.Decode(&cursorDoc); err != nil {
return nil, fmt.Errorf("fail to get result from cursor: %v", err)
}

if explainMode != "" {
// not really sensitive, but it's useless as the server version already appears
// in the footer of the site, so just remove it
delete(cursorDoc, "serverInfo")
delete(cursorDoc, "ok")

return mongoextjson.Marshal(cursorDoc)
}
// result doc looks like
//
// {"cursor":{"firstBatch":[{"_id":1},{"_id":2}],"id":NumberLong(0),"ns":"dbName.collection"},"ok":1}
docs := cursorDoc["cursor"].(bson.M)["firstBatch"].(bson.A)
if len(docs) == 0 {
return []byte(noDocFound), nil
}
Expand Down
Loading

0 comments on commit f7600ac

Please sign in to comment.