Skip to content

Commit b3cb998

Browse files
GT-409 Add support for explain API ([v1] and [V2]) (#502)
1 parent 0b8aa69 commit b3cb998

9 files changed

+387
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Fix test for extended names
1212
- Fix potential bug with DB name escaping for URL when requesting replication-related API
1313
- Retriable batch reads in AQL cursors
14+
- Add support for explain API ([v1] and [V2])
1415

1516
## [1.5.2](https://github.com/arangodb/go-driver/tree/v1.5.2) (2023-03-01)
1617
- Bump `DRIVER_VERSION`

database.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2023 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2017-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
// limitations under the License.
1717
//
1818
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
1920

2021
package driver
2122

@@ -67,6 +68,9 @@ type Database interface {
6768
// The query is not executed.
6869
ValidateQuery(ctx context.Context, query string) error
6970

71+
// ExplainQuery explains an AQL query and return information about it.
72+
ExplainQuery(ctx context.Context, query string, bindVars map[string]interface{}, opts *ExplainQueryOptions) (ExplainQueryResult, error)
73+
7074
// OptimizerRulesForQueries returns the available optimizer rules for AQL queries
7175
// returns an array of objects that contain the name of each available rule and its respective flags.
7276
OptimizerRulesForQueries(ctx context.Context) ([]QueryRule, error)

database_impl.go

+35
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,41 @@ func (d *database) ValidateQuery(ctx context.Context, query string) error {
182182
return nil
183183
}
184184

185+
// ExplainQuery explains an AQL query and return information about it.
186+
func (d *database) ExplainQuery(ctx context.Context, query string, bindVars map[string]interface{}, opts *ExplainQueryOptions) (ExplainQueryResult, error) {
187+
req, err := d.conn.NewRequest("POST", path.Join(d.relPath(), "_api/explain"))
188+
if err != nil {
189+
return ExplainQueryResult{}, WithStack(err)
190+
}
191+
input := struct {
192+
Query string `json:"query"`
193+
BindVars map[string]interface{} `json:"bindVars,omitempty"`
194+
Opts *ExplainQueryOptions `json:"options,omitempty"`
195+
}{
196+
Query: query,
197+
BindVars: bindVars,
198+
Opts: opts,
199+
}
200+
if _, err := req.SetBody(input); err != nil {
201+
return ExplainQueryResult{}, WithStack(err)
202+
}
203+
resp, err := d.conn.Do(ctx, req)
204+
if err != nil {
205+
return ExplainQueryResult{}, WithStack(err)
206+
}
207+
208+
var result ExplainQueryResult
209+
err = resp.ParseBody("", &result)
210+
if err != nil {
211+
return ExplainQueryResult{}, WithStack(err)
212+
}
213+
214+
if err := resp.CheckStatus(200); err != nil {
215+
return ExplainQueryResult{}, WithStack(err)
216+
}
217+
return result, nil
218+
}
219+
185220
// OptimizerRulesForQueries returns the available optimizer rules for AQL query
186221
// returns an array of objects that contain the name of each available rule and its respective flags.
187222
func (d *database) OptimizerRulesForQueries(ctx context.Context) ([]QueryRule, error) {

query_explain.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2023 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package driver
22+
23+
type ExplainQueryOptimizerOptions struct {
24+
// A list of to-be-included or to-be-excluded optimizer rules can be put into this attribute,
25+
// telling the optimizer to include or exclude specific rules.
26+
// To disable a rule, prefix its name with a "-", to enable a rule, prefix it with a "+".
27+
// There is also a pseudo-rule "all", which matches all optimizer rules. "-all" disables all rules.
28+
Rules []string `json:"rules,omitempty"`
29+
}
30+
31+
type ExplainQueryOptions struct {
32+
// If set to true, all possible execution plans will be returned.
33+
// The default is false, meaning only the optimal plan will be returned.
34+
AllPlans bool `json:"allPlans,omitempty"`
35+
36+
// An optional maximum number of plans that the optimizer is allowed to generate.
37+
// Setting this attribute to a low value allows to put a cap on the amount of work the optimizer does.
38+
MaxNumberOfPlans *int `json:"maxNumberOfPlans,omitempty"`
39+
40+
// Options related to the query optimizer.
41+
Optimizer ExplainQueryOptimizerOptions `json:"optimizer,omitempty"`
42+
}
43+
44+
type ExplainQueryResultExecutionNodeRaw map[string]interface{}
45+
type ExplainQueryResultExecutionCollection cursorPlanCollection
46+
type ExplainQueryResultExecutionVariable cursorPlanVariable
47+
48+
type ExplainQueryResultPlan struct {
49+
// Execution nodes of the plan.
50+
NodesRaw []ExplainQueryResultExecutionNodeRaw `json:"nodes,omitempty"`
51+
// List of rules the optimizer applied
52+
Rules []string `json:"rules,omitempty"`
53+
// List of collections used in the query
54+
Collections []ExplainQueryResultExecutionCollection `json:"collections,omitempty"`
55+
// List of variables used in the query (note: this may contain internal variables created by the optimizer)
56+
Variables []ExplainQueryResultExecutionVariable `json:"variables,omitempty"`
57+
// The total estimated cost for the plan. If there are multiple plans, the optimizer will choose the plan with the lowest total cost
58+
EstimatedCost float64 `json:"estimatedCost,omitempty"`
59+
// The estimated number of results.
60+
EstimatedNrItems int `json:"estimatedNrItems,omitempty"`
61+
}
62+
63+
type ExplainQueryResultExecutionStats struct {
64+
RulesExecuted int `json:"rulesExecuted,omitempty"`
65+
RulesSkipped int `json:"rulesSkipped,omitempty"`
66+
PlansCreated int `json:"plansCreated,omitempty"`
67+
PeakMemoryUsage uint64 `json:"peakMemoryUsage,omitempty"`
68+
ExecutionTime float64 `json:"executionTime,omitempty"`
69+
}
70+
71+
type ExplainQueryResult struct {
72+
Plan ExplainQueryResultPlan `json:"plan,omitempty"`
73+
Plans []ExplainQueryResultPlan `json:"plans,omitempty"`
74+
// List of warnings that occurred during optimization or execution plan creation
75+
Warnings []string `json:"warnings,omitempty"`
76+
// Info about optimizer statistics
77+
Stats ExplainQueryResultExecutionStats `json:"stats,omitempty"`
78+
// Cacheable states whether the query results can be cached on the server if the query result cache were used.
79+
// This attribute is not present when allPlans is set to true.
80+
Cacheable *bool `json:"cacheable,omitempty"`
81+
}

test/query_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,69 @@ func TestValidateQuery(t *testing.T) {
137137
}
138138
}
139139

140+
// TestExplainQuery tries to explain several AQL queries.
141+
func TestExplainQuery(t *testing.T) {
142+
ctx := context.Background()
143+
c := createClientFromEnv(t, true)
144+
db := ensureDatabase(ctx, c, "explain_query_test", nil, t)
145+
146+
db, clean := prepareQueryDatabase(t, ctx, c, "explain_query_test")
147+
defer clean(t)
148+
149+
// Setup tests
150+
tests := []struct {
151+
Query string
152+
BindVars map[string]interface{}
153+
Opts *driver.ExplainQueryOptions
154+
ExpectSuccess bool
155+
}{
156+
{
157+
Query: "FOR d IN books SORT d.Title RETURN d",
158+
ExpectSuccess: true,
159+
},
160+
{
161+
Query: "FOR d IN books FILTER d.Title==@title SORT d.Title RETURN d",
162+
BindVars: map[string]interface{}{
163+
"title": "Defending the Undefendable",
164+
},
165+
ExpectSuccess: true,
166+
},
167+
{
168+
Query: "FOR d IN books FILTER d.Title==@title SORT d.Title RETURN d",
169+
BindVars: map[string]interface{}{
170+
"title": "Democracy: God That Failed",
171+
},
172+
Opts: &driver.ExplainQueryOptions{
173+
AllPlans: true,
174+
Optimizer: driver.ExplainQueryOptimizerOptions{},
175+
},
176+
ExpectSuccess: true,
177+
},
178+
{
179+
Query: "FOR d IN books FILTER d.Title==@title SORT d.Title RETURN d",
180+
ExpectSuccess: false, // bindVars not provided
181+
},
182+
{
183+
Query: "FOR u IN users FILTER u.age>>>100 SORT u.name RETURN u",
184+
ExpectSuccess: false, // syntax error
185+
},
186+
{
187+
Query: "",
188+
ExpectSuccess: false,
189+
},
190+
}
191+
for i, test := range tests {
192+
t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) {
193+
_, err := db.ExplainQuery(ctx, test.Query, test.BindVars, test.Opts)
194+
if test.ExpectSuccess {
195+
require.NoError(t, err, "case %d", i)
196+
} else {
197+
require.Error(t, err, "case %d", i)
198+
}
199+
})
200+
}
201+
}
202+
140203
// TestValidateQuery validates several AQL queries.
141204
func TestValidateQueryOptionShardIds(t *testing.T) {
142205
ctx := context.Background()

v2/arangodb/database.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2020 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2020-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.

v2/arangodb/database_query.go

+73
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type DatabaseQuery interface {
3333
// When the query is valid, nil returned, otherwise an error is returned.
3434
// The query is not executed.
3535
ValidateQuery(ctx context.Context, query string) error
36+
37+
// ExplainQuery explains an AQL query and return information about it.
38+
ExplainQuery(ctx context.Context, query string, bindVars map[string]interface{}, opts *ExplainQueryOptions) (ExplainQueryResult, error)
3639
}
3740

3841
type QuerySubOptions struct {
@@ -99,3 +102,73 @@ type QueryOptions struct {
99102
type QueryRequest struct {
100103
Query string `json:"query"`
101104
}
105+
106+
type ExplainQueryOptimizerOptions struct {
107+
// A list of to-be-included or to-be-excluded optimizer rules can be put into this attribute,
108+
// telling the optimizer to include or exclude specific rules.
109+
// To disable a rule, prefix its name with a "-", to enable a rule, prefix it with a "+".
110+
// There is also a pseudo-rule "all", which matches all optimizer rules. "-all" disables all rules.
111+
Rules []string `json:"rules,omitempty"`
112+
}
113+
114+
type ExplainQueryOptions struct {
115+
// If set to true, all possible execution plans will be returned.
116+
// The default is false, meaning only the optimal plan will be returned.
117+
AllPlans bool `json:"allPlans,omitempty"`
118+
119+
// An optional maximum number of plans that the optimizer is allowed to generate.
120+
// Setting this attribute to a low value allows to put a cap on the amount of work the optimizer does.
121+
MaxNumberOfPlans *int `json:"maxNumberOfPlans,omitempty"`
122+
123+
// Options related to the query optimizer.
124+
Optimizer ExplainQueryOptimizerOptions `json:"optimizer,omitempty"`
125+
}
126+
127+
type ExplainQueryResultExecutionNodeRaw map[string]interface{}
128+
129+
type ExplainQueryResultExecutionCollection struct {
130+
Name string `json:"name"`
131+
Type string `json:"type"`
132+
}
133+
134+
type ExplainQueryResultExecutionVariable struct {
135+
ID int `json:"id"`
136+
Name string `json:"name"`
137+
IsDataFromCollection bool `json:"isDataFromCollection"`
138+
IsFullDocumentFromCollection bool `json:"isFullDocumentFromCollection"`
139+
}
140+
141+
type ExplainQueryResultPlan struct {
142+
// Execution nodes of the plan.
143+
NodesRaw []ExplainQueryResultExecutionNodeRaw `json:"nodes,omitempty"`
144+
// List of rules the optimizer applied
145+
Rules []string `json:"rules,omitempty"`
146+
// List of collections used in the query
147+
Collections []ExplainQueryResultExecutionCollection `json:"collections,omitempty"`
148+
// List of variables used in the query (note: this may contain internal variables created by the optimizer)
149+
Variables []ExplainQueryResultExecutionVariable `json:"variables,omitempty"`
150+
// The total estimated cost for the plan. If there are multiple plans, the optimizer will choose the plan with the lowest total cost
151+
EstimatedCost float64 `json:"estimatedCost,omitempty"`
152+
// The estimated number of results.
153+
EstimatedNrItems int `json:"estimatedNrItems,omitempty"`
154+
}
155+
156+
type ExplainQueryResultExecutionStats struct {
157+
RulesExecuted int `json:"rulesExecuted,omitempty"`
158+
RulesSkipped int `json:"rulesSkipped,omitempty"`
159+
PlansCreated int `json:"plansCreated,omitempty"`
160+
PeakMemoryUsage uint64 `json:"peakMemoryUsage,omitempty"`
161+
ExecutionTime float64 `json:"executionTime,omitempty"`
162+
}
163+
164+
type ExplainQueryResult struct {
165+
Plan ExplainQueryResultPlan `json:"plan,omitempty"`
166+
Plans []ExplainQueryResultPlan `json:"plans,omitempty"`
167+
// List of warnings that occurred during optimization or execution plan creation
168+
Warnings []string `json:"warnings,omitempty"`
169+
// Info about optimizer statistics
170+
Stats ExplainQueryResultExecutionStats `json:"stats,omitempty"`
171+
// Cacheable states whether the query results can be cached on the server if the query result cache were used.
172+
// This attribute is not present when allPlans is set to true.
173+
Cacheable *bool `json:"cacheable,omitempty"`
174+
}

v2/arangodb/database_query_impl.go

+29
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,32 @@ func (d databaseQuery) ValidateQuery(ctx context.Context, query string) error {
9393
return response.AsArangoErrorWithCode(code)
9494
}
9595
}
96+
97+
func (d databaseQuery) ExplainQuery(ctx context.Context, query string, bindVars map[string]interface{}, opts *ExplainQueryOptions) (ExplainQueryResult, error) {
98+
url := d.db.url("_api", "explain")
99+
100+
var request = struct {
101+
Query string `json:"query"`
102+
BindVars map[string]interface{} `json:"bindVars,omitempty"`
103+
Opts *ExplainQueryOptions `json:"options,omitempty"`
104+
}{
105+
Query: query,
106+
BindVars: bindVars,
107+
Opts: opts,
108+
}
109+
var response struct {
110+
shared.ResponseStruct `json:",inline"`
111+
ExplainQueryResult
112+
}
113+
resp, err := connection.CallPost(ctx, d.db.connection(), url, &response, &request, d.db.modifiers...)
114+
if err != nil {
115+
return ExplainQueryResult{}, err
116+
}
117+
118+
switch code := resp.Code(); code {
119+
case http.StatusOK:
120+
return response.ExplainQueryResult, nil
121+
default:
122+
return ExplainQueryResult{}, response.AsArangoErrorWithCode(code)
123+
}
124+
}

0 commit comments

Comments
 (0)