Skip to content

Commit 8cec7ac

Browse files
authored
Merge pull request #699 from ydb-platform/explain
Add explain method to query session and pool
2 parents a2536ff + 9fd9792 commit 8cec7ac

16 files changed

+287
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Add method explain for explain query to QuerySession and QuerySessionPool classes
2+
13
## 3.21.8 ##
24
* Fix: convert gRPC stream termination to YDB errors in async query client
35

docker-compose-tls.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,4 @@ services:
1010
volumes:
1111
- ./ydb_certs:/ydb_certs
1212
environment:
13-
- YDB_USE_IN_MEMORY_PDISKS=true
1413
- YDB_ENABLE_COLUMN_TABLES=true

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ services:
77
- 2136:2136
88
hostname: localhost
99
environment:
10-
- YDB_USE_IN_MEMORY_PDISKS=true
1110
- YDB_ENABLE_COLUMN_TABLES=true

tests/aio/query/test_query_session.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import json
2+
13
import pytest
24

35
import ydb
6+
from ydb import QueryExplainResultFormat
47
from ydb.aio.query.session import QuerySession
58

69

@@ -125,3 +128,37 @@ async def test_terminated_stream_raises_ydb_error(self, session: QuerySession):
125128
async with await session.execute("select 1") as results:
126129
async for _ in results:
127130
pass
131+
132+
@pytest.mark.asyncio
133+
async def test_explain(self, pool: ydb.aio.query.QuerySessionPool):
134+
await pool.execute_with_retries("DROP TABLE IF EXISTS test_explain")
135+
await pool.execute_with_retries("CREATE TABLE test_explain (id Int64, PRIMARY KEY (id))")
136+
try:
137+
plan_fullscan = ""
138+
plan_lookup = ""
139+
140+
async def callee(session: QuerySession):
141+
nonlocal plan_fullscan, plan_lookup
142+
143+
plan = await session.explain("SELECT * FROM test_explain", result_format=QueryExplainResultFormat.STR)
144+
isinstance(plan, str)
145+
assert "FullScan" in plan
146+
147+
plan_fullscan = await session.explain(
148+
"SELECT * FROM test_explain", result_format=QueryExplainResultFormat.DICT
149+
)
150+
plan_lookup = await session.explain(
151+
"SELECT * FROM test_explain WHERE id = $id",
152+
{"$id": 1},
153+
result_format=QueryExplainResultFormat.DICT,
154+
)
155+
156+
await pool.retry_operation_async(callee)
157+
158+
plan_fulltext_string = json.dumps(plan_fullscan)
159+
assert "FullScan" in plan_fulltext_string
160+
161+
plan_lookup_string = json.dumps(plan_lookup)
162+
assert "Lookup" in plan_lookup_string
163+
finally:
164+
await pool.execute_with_retries("DROP TABLE test_explain")

tests/aio/query/test_query_session_pool.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import asyncio
2+
import json
3+
24
import pytest
35
import ydb
46

57
from typing import Optional
68

9+
from tests.conftest import wait_container_ready_async
10+
from ydb import QueryExplainResultFormat
711
from ydb.aio.query.pool import QuerySessionPool
812
from ydb.aio.query.session import QuerySession, QuerySessionStateEnum
913
from ydb.aio.query.transaction import QueryTxContext
@@ -162,6 +166,7 @@ async def test_no_session_leak(self, driver, docker_project):
162166

163167
docker_project.start()
164168
await pool.stop()
169+
await wait_container_ready_async(driver)
165170

166171
@pytest.mark.asyncio
167172
async def test_acquire_no_race_condition(self, driver):
@@ -179,3 +184,30 @@ async def acquire_session():
179184

180185
assert len(ids) == 1
181186
assert pool._current_size == 1
187+
188+
@pytest.mark.asyncio
189+
async def test_explain_with_retries(self, pool: QuerySessionPool):
190+
await pool.execute_with_retries("DROP TABLE IF EXISTS test_explain")
191+
await pool.execute_with_retries("CREATE TABLE test_explain (id Int64, PRIMARY KEY (id))")
192+
try:
193+
plan = await pool.explain_with_retries(
194+
"SELECT * FROM test_explain", result_format=QueryExplainResultFormat.STR
195+
)
196+
isinstance(plan, str)
197+
assert "FullScan" in plan
198+
199+
plan = await pool.explain_with_retries(
200+
"SELECT * FROM test_explain", result_format=QueryExplainResultFormat.DICT
201+
)
202+
plan_string = json.dumps(plan)
203+
assert "FullScan" in plan_string
204+
205+
plan = await pool.explain_with_retries(
206+
"SELECT * FROM test_explain WHERE id = $id",
207+
{"$id": 1},
208+
result_format=QueryExplainResultFormat.DICT,
209+
)
210+
plan_string = json.dumps(plan)
211+
assert "Lookup" in plan_string
212+
finally:
213+
await pool.execute_with_retries("DROP TABLE test_explain")

tests/aio/test_connection_pool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
from unittest.mock import MagicMock
33

4+
from tests.conftest import wait_container_ready_async
45
from ydb.aio.driver import Driver
56
import pytest
67
import ydb
@@ -108,6 +109,7 @@ async def restart_docker():
108109
await asyncio.gather(*coros, return_exceptions=False)
109110

110111
docker_project.start()
112+
await wait_container_ready_async(driver)
111113
await driver.stop()
112114

113115

@@ -132,6 +134,7 @@ async def test_disconnect_by_call(endpoint, database, docker_project):
132134
await asyncio.sleep(5)
133135
assert len(driver._store.connections) == 0
134136
docker_project.start()
137+
await wait_container_ready_async(driver)
135138
await driver.stop()
136139

137140

tests/aio/test_session_pool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
import ydb
5+
from tests.conftest import wait_container_ready_async
56

67

78
@pytest.mark.asyncio
@@ -90,6 +91,7 @@ async def test_no_cluster_endpoints_no_failure(driver, docker_project):
9091
await pool.release(sess)
9192
assert pool._active_count == 0
9293
await pool.stop()
94+
await wait_container_ready_async(driver)
9395

9496

9597
@pytest.mark.asyncio
@@ -144,3 +146,4 @@ async def test_no_session_leak(driver, docker_project):
144146

145147
docker_project.start()
146148
await pool.stop()
149+
await wait_container_ready_async(driver)

tests/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import os
23

34
import pytest
@@ -30,6 +31,25 @@ def wait_container_ready(driver):
3031
raise RuntimeError("Container is not ready after timeout.")
3132

3233

34+
async def wait_container_ready_async(driver):
35+
await driver.wait(timeout=30)
36+
37+
async with ydb.aio.SessionPool(driver, 1) as pool:
38+
39+
started_at = time.time()
40+
while time.time() - started_at < 30:
41+
try:
42+
async with pool.checkout() as session:
43+
await session.execute_scheme("create table `.sys_health/test_table` (A int32, primary key(A));")
44+
45+
return True
46+
47+
except ydb.Error:
48+
await asyncio.sleep(1)
49+
50+
raise RuntimeError("Container is not ready after timeout.")
51+
52+
3353
@pytest.fixture(scope="module")
3454
def endpoint(pytestconfig, module_scoped_container_getter):
3555
with ydb.Driver(endpoint="localhost:2136", database="/local") as driver:

tests/query/test_query_session.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import json
2+
13
import pytest
24
import threading
35
import time
46
from concurrent.futures import _base as b
57
from unittest import mock
68

7-
from ydb.query.base import QueryStatsMode
9+
from ydb import QuerySessionPool
10+
from ydb.query.base import QueryStatsMode, QueryExplainResultFormat
811
from ydb.query.session import QuerySession
912

1013

@@ -174,3 +177,37 @@ def test_stats_mode(self, session: QuerySession, stats_mode: QueryStatsMode):
174177
assert len(stats.query_plan) > 0
175178
else:
176179
assert stats.query_plan == ""
180+
181+
def test_explain(self, pool: QuerySessionPool):
182+
pool.execute_with_retries("DROP TABLE IF EXISTS test_explain")
183+
pool.execute_with_retries("CREATE TABLE test_explain (id Int64, PRIMARY KEY (id))")
184+
try:
185+
plan_fullscan = ""
186+
plan_lookup = ""
187+
188+
def callee(session: QuerySession):
189+
nonlocal plan_fullscan, plan_lookup
190+
191+
plan = session.explain("SELECT * FROM test_explain", result_format=QueryExplainResultFormat.STR)
192+
isinstance(plan, str)
193+
assert "FullScan" in plan
194+
195+
plan_fullscan = session.explain(
196+
"SELECT * FROM test_explain", result_format=QueryExplainResultFormat.DICT
197+
)
198+
199+
plan_lookup = session.explain(
200+
"SELECT * FROM test_explain WHERE id = $id",
201+
{"$id": 1},
202+
result_format=QueryExplainResultFormat.DICT,
203+
)
204+
205+
pool.retry_operation_sync(callee)
206+
207+
plan_fulltext_string = json.dumps(plan_fullscan)
208+
assert "FullScan" in plan_fulltext_string
209+
210+
plan_lookup_string = json.dumps(plan_lookup)
211+
assert "Lookup" in plan_lookup_string
212+
finally:
213+
pool.execute_with_retries("DROP TABLE test_explain")

tests/query/test_query_session_pool.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import json
2+
13
import pytest
24
import ydb
35
import time
46
from concurrent import futures
57

68
from typing import Optional
79

10+
from tests.conftest import wait_container_ready
11+
from ydb import QueryExplainResultFormat
812
from ydb.query.pool import QuerySessionPool
913
from ydb.query.session import QuerySession, QuerySessionStateEnum
1014
from ydb.query.transaction import QueryTxContext
@@ -147,6 +151,7 @@ def test_no_session_leak(self, driver_sync, docker_project):
147151
assert pool._current_size == 0
148152

149153
docker_project.start()
154+
wait_container_ready(driver_sync)
150155
pool.stop()
151156

152157
def test_execute_with_retries_async(self, pool: QuerySessionPool):
@@ -200,3 +205,28 @@ def test_async_methods_after_stop_raise(self, pool: QuerySessionPool):
200205
pool.stop()
201206
with pytest.raises(ydb.SessionPoolClosed):
202207
pool.execute_with_retries_async("select 1;")
208+
209+
def test_explain_with_retries(self, pool: QuerySessionPool):
210+
pool.execute_with_retries("DROP TABLE IF EXISTS test_explain")
211+
pool.execute_with_retries("CREATE TABLE test_explain (id Int64, PRIMARY KEY (id))")
212+
try:
213+
214+
plan = pool.explain_with_retries("SELECT * FROM test_explain", result_format=QueryExplainResultFormat.STR)
215+
isinstance(plan, str)
216+
assert "FullScan" in plan
217+
218+
plan = pool.explain_with_retries("SELECT * FROM test_explain", result_format=QueryExplainResultFormat.DICT)
219+
assert isinstance(plan, dict)
220+
221+
plan_string = json.dumps(plan)
222+
assert "FullScan" in plan_string
223+
224+
plan = pool.explain_with_retries(
225+
"SELECT * FROM test_explain WHERE id = $id",
226+
{"$id": 1},
227+
result_format=ydb.QueryExplainResultFormat.DICT,
228+
)
229+
plan_string = json.dumps(plan)
230+
assert "Lookup" in plan_string
231+
finally:
232+
pool.execute_with_retries("DROP TABLE test_explain")

0 commit comments

Comments
 (0)