Skip to content

Commit 0c0adb9

Browse files
Merge pull request #251 from ArangoDB-Community/feature/de-546-stats-field-in-explain-api
[DE-546] Adding stats fields to the explain API
2 parents 4a45183 + 52648c7 commit 0c0adb9

File tree

8 files changed

+74
-16
lines changed

8 files changed

+74
-16
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
main
2+
----
3+
4+
* Adding peak_memory_usage as a new property of AQL queries, available since ArangoDB 3.11.
5+
6+
* The explain method of AQL queries includes the "stats" field in the returned object. Note that the REST API returns
7+
it separately from the "plan" field, but for now we have to merge them together to ensure backward compatibility.

arango/aql.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,19 @@ def response_handler(resp: Response) -> Union[Json, Jsons]:
213213
if not resp.is_success:
214214
raise AQLQueryExplainError(resp, request)
215215
if "plan" in resp.body:
216-
plan: Json = resp.body["plan"]
217-
return plan
216+
result: Json = resp.body["plan"]
217+
if "stats" in resp.body:
218+
result["stats"] = resp.body["stats"]
219+
return result
218220
else:
219-
plans: Jsons = resp.body["plans"]
220-
return plans
221+
results: Jsons = resp.body["plans"]
222+
if "stats" in resp.body:
223+
# Although "plans" contains an array, "stats" is a single object.
224+
# We need to duplicate "stats" for each plan in order to preserve
225+
# the original structure.
226+
for plan in results:
227+
plan["stats"] = resp.body["stats"]
228+
return results
221229

222230
return self._execute(request, response_handler)
223231

arango/formatter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ def format_aql_query(body: Json) -> Json:
322322
result["stream"] = body["stream"]
323323
if "user" in body:
324324
result["user"] = body["user"]
325+
326+
# New in 3.11
327+
if "peakMemoryUsage" in body:
328+
result["peak_memory_usage"] = body["peakMemoryUsage"]
325329
return verify_format(body, result)
326330

327331

arango/request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def normalize_headers(
1212
if driver_flags is not None:
1313
for flag in driver_flags:
1414
flags = flags + flag + ";"
15-
driver_version = "7.5.3"
15+
driver_version = "7.5.8"
1616
driver_header = "python-arango/" + driver_version + " (" + flags + ")"
1717
normalized_headers: Headers = {
1818
"charset": "utf-8",

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"PyJWT",
2525
"setuptools>=42",
2626
"importlib_metadata>=4.7.1",
27+
"packaging>=23.1",
2728
],
2829
extras_require={
2930
"dev": [

tester.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ if [[ "$tests" != "all" && "$tests" != "community" && "$tests" != "enterprise" ]
2121
exit 1
2222
fi
2323

24-
version="${3:-3.10.6}"
24+
# 3.11.0
25+
# 3.10.6
26+
# 3.9.9
27+
version="${3:-3.11.0}"
2528

2629
if [[ -n "$4" && "$4" != "notest" ]]; then
2730
echo "Invalid argument. Use 'notest' to only start the docker container, without running the tests."

tests/conftest.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22

33
import pytest
4+
from packaging import version
45

56
from arango import ArangoClient, formatter
67
from arango.database import StandardDatabase
@@ -40,6 +41,7 @@ class GlobalData:
4041
enterprise: bool = None
4142
secret: str = None
4243
root_password: str = None
44+
db_version: version = version.parse("0.0.0")
4345

4446

4547
global_data = GlobalData()
@@ -66,7 +68,7 @@ def pytest_configure(config):
6668
password=config.getoption("passwd"),
6769
superuser_token=generate_jwt(secret),
6870
)
69-
sys_db.version()
71+
db_version = sys_db.version()
7072

7173
# Create a user and non-system database for testing.
7274
username = generate_username()
@@ -118,6 +120,7 @@ def pytest_configure(config):
118120
global_data.username = username
119121
global_data.password = password
120122
global_data.db_name = tst_db_name
123+
global_data.db_version = version.parse(db_version)
121124
global_data.sys_db = sys_db
122125
global_data.tst_db = tst_db
123126
global_data.bad_db = bad_db
@@ -247,6 +250,11 @@ def mock_verify_format(body, result):
247250
monkeypatch.setattr(formatter, "verify_format", mock_verify_format)
248251

249252

253+
@pytest.fixture(autouse=False)
254+
def db_version():
255+
return global_data.db_version
256+
257+
250258
@pytest.fixture(autouse=False)
251259
def url():
252260
return global_data.url

tests/test_aql.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from packaging import version
2+
13
from arango.exceptions import (
24
AQLCacheClearError,
35
AQLCacheConfigureError,
@@ -26,37 +28,60 @@ def test_aql_attributes(db, username):
2628
assert repr(db.aql.cache) == f"<AQLQueryCache in {db.name}>"
2729

2830

29-
def test_aql_query_management(db, bad_db, col, docs):
30-
plan_fields = [
31+
def test_aql_query_management(db_version, db, bad_db, col, docs):
32+
explain_fields = [
3133
"estimatedNrItems",
3234
"estimatedCost",
3335
"rules",
3436
"variables",
3537
"collections",
38+
"stats",
3639
]
40+
stats_fields = {
41+
"0.0.0": [
42+
"rulesExecuted",
43+
"rulesSkipped",
44+
"plansCreated",
45+
],
46+
"3.10.4": [
47+
"peakMemoryUsage",
48+
"executionTime",
49+
],
50+
}
51+
3752
# Test explain invalid query
3853
with assert_raises(AQLQueryExplainError) as err:
3954
db.aql.explain("INVALID QUERY")
4055
assert err.value.error_code == 1501
4156

4257
# Test explain valid query with all_plans set to False
43-
plan = db.aql.explain(
58+
explain = db.aql.explain(
4459
f"FOR d IN {col.name} RETURN d",
4560
all_plans=False,
4661
opt_rules=["-all", "+use-index-range"],
4762
)
48-
assert all(field in plan for field in plan_fields)
63+
assert all(field in explain for field in explain_fields)
64+
for v, fields in stats_fields.items():
65+
if db_version >= version.parse(v):
66+
assert all(field in explain["stats"] for field in fields)
67+
else:
68+
assert all(field not in explain["stats"] for field in fields)
4969

5070
# Test explain valid query with all_plans set to True
51-
plans = db.aql.explain(
71+
explanations = db.aql.explain(
5272
f"FOR d IN {col.name} RETURN d",
5373
all_plans=True,
5474
opt_rules=["-all", "+use-index-range"],
5575
max_plans=10,
5676
)
57-
for plan in plans:
58-
assert all(field in plan for field in plan_fields)
59-
assert len(plans) < 10
77+
for explain in explanations:
78+
assert all(field in explain for field in explain_fields)
79+
for v, fields in stats_fields.items():
80+
if db_version >= version.parse(v):
81+
assert all(field in explain["stats"] for field in fields)
82+
else:
83+
assert all(field not in explain["stats"] for field in fields)
84+
assert len(explanations) < 10
6085

6186
# Test validate invalid query
6287
with assert_raises(AQLQueryValidateError) as err:
@@ -161,7 +186,7 @@ def test_aql_query_management(db, bad_db, col, docs):
161186
assert new_tracking["track_bind_vars"] is True
162187
assert new_tracking["track_slow_queries"] is True
163188

164-
# Kick off some long lasting queries in the background
189+
# Kick off some long-lasting queries in the background
165190
db.begin_async_execution().aql.execute("RETURN SLEEP(100)")
166191
db.begin_async_execution().aql.execute("RETURN SLEEP(50)")
167192

@@ -174,6 +199,8 @@ def test_aql_query_management(db, bad_db, col, docs):
174199
assert "state" in query
175200
assert "bind_vars" in query
176201
assert "runtime" in query
202+
if db_version >= version.parse("3.11"):
203+
assert "peak_memory_usage" in query
177204
assert len(queries) == 2
178205

179206
# Test list queries with bad database

0 commit comments

Comments
 (0)