Skip to content

Commit afcb2f8

Browse files
authored
Merge pull request #455 from ydb-platform/query_service
Query service client support
2 parents c383b40 + 49e7471 commit afcb2f8

22 files changed

+1988
-140
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: "3.3"
22
services:
33
ydb:
4-
image: cr.yandex/yc/yandex-docker-local-ydb:latest
4+
image: ydbplatform/local-ydb:trunk
55
restart: always
66
ports:
77
- 2136:2136
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import ydb
2+
3+
4+
def main():
5+
driver_config = ydb.DriverConfig(
6+
endpoint="grpc://localhost:2136",
7+
database="/local",
8+
# credentials=ydb.credentials_from_env_variables(),
9+
# root_certificates=ydb.load_ydb_root_certificate(),
10+
)
11+
try:
12+
driver = ydb.Driver(driver_config)
13+
driver.wait(timeout=5)
14+
except TimeoutError:
15+
raise RuntimeError("Connect failed to YDB")
16+
17+
pool = ydb.QuerySessionPool(driver)
18+
19+
print("=" * 50)
20+
print("DELETE TABLE IF EXISTS")
21+
pool.execute_with_retries("DROP TABLE IF EXISTS example")
22+
23+
print("=" * 50)
24+
print("CREATE TABLE")
25+
pool.execute_with_retries("CREATE TABLE example(key UInt64, value String, PRIMARY KEY (key))")
26+
27+
pool.execute_with_retries("INSERT INTO example (key, value) VALUES (1, 'onepieceisreal')")
28+
29+
def callee(session):
30+
print("=" * 50)
31+
with session.execute("DELETE FROM example"):
32+
pass
33+
34+
print("BEFORE ACTION")
35+
with session.execute("SELECT COUNT(*) AS rows_count FROM example") as results:
36+
for result_set in results:
37+
print(f"rows: {str(result_set.rows)}")
38+
39+
print("=" * 50)
40+
print("INSERT WITH COMMIT TX")
41+
42+
with session.transaction() as tx:
43+
tx.begin()
44+
45+
with tx.execute("INSERT INTO example (key, value) VALUES (1, 'onepieceisreal')"):
46+
pass
47+
48+
with tx.execute("SELECT COUNT(*) AS rows_count FROM example") as results:
49+
for result_set in results:
50+
print(f"rows: {str(result_set.rows)}")
51+
52+
tx.commit()
53+
54+
print("=" * 50)
55+
print("AFTER COMMIT TX")
56+
57+
with session.execute("SELECT COUNT(*) AS rows_count FROM example") as results:
58+
for result_set in results:
59+
print(f"rows: {str(result_set.rows)}")
60+
61+
print("=" * 50)
62+
print("INSERT WITH ROLLBACK TX")
63+
64+
with session.transaction() as tx:
65+
tx.begin()
66+
67+
with tx.execute("INSERT INTO example (key, value) VALUES (2, 'onepieceisreal')"):
68+
pass
69+
70+
with tx.execute("SELECT COUNT(*) AS rows_count FROM example") as results:
71+
for result_set in results:
72+
print(f"rows: {str(result_set.rows)}")
73+
74+
tx.rollback()
75+
76+
print("=" * 50)
77+
print("AFTER ROLLBACK TX")
78+
79+
with session.execute("SELECT COUNT(*) AS rows_count FROM example") as results:
80+
for result_set in results:
81+
print(f"rows: {str(result_set.rows)}")
82+
83+
pool.retry_operation_sync(callee)
84+
85+
86+
if __name__ == "__main__":
87+
main()

tests/query/__init__.py

Whitespace-only changes.

tests/query/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
from ydb.query.session import QuerySessionSync
3+
from ydb.query.pool import QuerySessionPool
4+
5+
6+
@pytest.fixture
7+
def session(driver_sync):
8+
session = QuerySessionSync(driver_sync)
9+
10+
yield session
11+
12+
try:
13+
session.delete()
14+
except BaseException:
15+
pass
16+
17+
18+
@pytest.fixture
19+
def tx(session):
20+
session.create()
21+
transaction = session.transaction()
22+
23+
yield transaction
24+
25+
try:
26+
transaction.rollback()
27+
except BaseException:
28+
pass
29+
30+
31+
@pytest.fixture
32+
def pool(driver_sync):
33+
pool = QuerySessionPool(driver_sync)
34+
yield pool

tests/query/test_query_session.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import pytest
2+
3+
from ydb.query.session import QuerySessionSync
4+
5+
6+
def _check_session_state_empty(session: QuerySessionSync):
7+
assert session._state.session_id is None
8+
assert session._state.node_id is None
9+
assert not session._state.attached
10+
11+
12+
def _check_session_state_full(session: QuerySessionSync):
13+
assert session._state.session_id is not None
14+
assert session._state.node_id is not None
15+
assert session._state.attached
16+
17+
18+
class TestQuerySession:
19+
def test_session_normal_lifecycle(self, session: QuerySessionSync):
20+
_check_session_state_empty(session)
21+
22+
session.create()
23+
_check_session_state_full(session)
24+
25+
session.delete()
26+
_check_session_state_empty(session)
27+
28+
def test_second_create_do_nothing(self, session: QuerySessionSync):
29+
session.create()
30+
_check_session_state_full(session)
31+
32+
session_id_before = session._state.session_id
33+
node_id_before = session._state.node_id
34+
35+
session.create()
36+
_check_session_state_full(session)
37+
38+
assert session._state.session_id == session_id_before
39+
assert session._state.node_id == node_id_before
40+
41+
def test_second_delete_do_nothing(self, session: QuerySessionSync):
42+
session.create()
43+
44+
session.delete()
45+
session.delete()
46+
47+
def test_delete_before_create_not_possible(self, session: QuerySessionSync):
48+
with pytest.raises(RuntimeError):
49+
session.delete()
50+
51+
def test_create_after_delete_not_possible(self, session: QuerySessionSync):
52+
session.create()
53+
session.delete()
54+
with pytest.raises(RuntimeError):
55+
session.create()
56+
57+
def test_transaction_before_create_raises(self, session: QuerySessionSync):
58+
with pytest.raises(RuntimeError):
59+
session.transaction()
60+
61+
def test_transaction_after_delete_raises(self, session: QuerySessionSync):
62+
session.create()
63+
64+
session.delete()
65+
66+
with pytest.raises(RuntimeError):
67+
session.transaction()
68+
69+
def test_transaction_after_create_not_raises(self, session: QuerySessionSync):
70+
session.create()
71+
session.transaction()
72+
73+
def test_execute_before_create_raises(self, session: QuerySessionSync):
74+
with pytest.raises(RuntimeError):
75+
session.execute("select 1;")
76+
77+
def test_execute_after_delete_raises(self, session: QuerySessionSync):
78+
session.create()
79+
session.delete()
80+
with pytest.raises(RuntimeError):
81+
session.execute("select 1;")
82+
83+
def test_basic_execute(self, session: QuerySessionSync):
84+
session.create()
85+
it = session.execute("select 1;")
86+
result_sets = [result_set for result_set in it]
87+
88+
assert len(result_sets) == 1
89+
assert len(result_sets[0].rows) == 1
90+
assert len(result_sets[0].columns) == 1
91+
assert list(result_sets[0].rows[0].values()) == [1]
92+
93+
def test_two_results(self, session: QuerySessionSync):
94+
session.create()
95+
res = []
96+
97+
with session.execute("select 1; select 2") as results:
98+
for result_set in results:
99+
if len(result_set.rows) > 0:
100+
res.append(list(result_set.rows[0].values()))
101+
102+
assert res == [[1], [2]]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
import ydb
3+
from ydb.query.pool import QuerySessionPool
4+
from ydb.query.session import QuerySessionSync, QuerySessionStateEnum
5+
6+
7+
class TestQuerySessionPool:
8+
def test_checkout_provides_created_session(self, pool: QuerySessionPool):
9+
with pool.checkout() as session:
10+
assert session._state._state == QuerySessionStateEnum.CREATED
11+
12+
assert session._state._state == QuerySessionStateEnum.CLOSED
13+
14+
def test_oneshot_query_normal(self, pool: QuerySessionPool):
15+
res = pool.execute_with_retries("select 1;")
16+
assert len(res) == 1
17+
18+
def test_oneshot_ddl_query(self, pool: QuerySessionPool):
19+
pool.execute_with_retries("create table Queen(key UInt64, PRIMARY KEY (key));")
20+
pool.execute_with_retries("drop table Queen;")
21+
22+
def test_oneshot_query_raises(self, pool: QuerySessionPool):
23+
with pytest.raises(ydb.GenericError):
24+
pool.execute_with_retries("Is this the real life? Is this just fantasy?")
25+
26+
def test_retry_op_uses_created_session(self, pool: QuerySessionPool):
27+
def callee(session: QuerySessionSync):
28+
assert session._state._state == QuerySessionStateEnum.CREATED
29+
30+
pool.retry_operation_sync(callee)
31+
32+
def test_retry_op_normal(self, pool: QuerySessionPool):
33+
def callee(session: QuerySessionSync):
34+
with session.transaction() as tx:
35+
iterator = tx.execute("select 1;", commit_tx=True)
36+
return [result_set for result_set in iterator]
37+
38+
res = pool.retry_operation_sync(callee)
39+
assert len(res) == 1
40+
41+
def test_retry_op_raises(self, pool: QuerySessionPool):
42+
class CustomException(Exception):
43+
pass
44+
45+
def callee(session: QuerySessionSync):
46+
raise CustomException()
47+
48+
with pytest.raises(CustomException):
49+
pool.retry_operation_sync(callee)

tests/query/test_query_transaction.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
3+
from ydb.query.transaction import BaseQueryTxContext
4+
from ydb.query.transaction import QueryTxStateEnum
5+
6+
7+
class TestQueryTransaction:
8+
def test_tx_begin(self, tx: BaseQueryTxContext):
9+
assert tx.tx_id is None
10+
11+
tx.begin()
12+
assert tx.tx_id is not None
13+
14+
def test_tx_allow_double_commit(self, tx: BaseQueryTxContext):
15+
tx.begin()
16+
tx.commit()
17+
tx.commit()
18+
19+
def test_tx_allow_double_rollback(self, tx: BaseQueryTxContext):
20+
tx.begin()
21+
tx.rollback()
22+
tx.rollback()
23+
24+
def test_tx_commit_before_begin(self, tx: BaseQueryTxContext):
25+
tx.commit()
26+
assert tx._tx_state._state == QueryTxStateEnum.COMMITTED
27+
28+
def test_tx_rollback_before_begin(self, tx: BaseQueryTxContext):
29+
tx.rollback()
30+
assert tx._tx_state._state == QueryTxStateEnum.ROLLBACKED
31+
32+
def test_tx_first_execute_begins_tx(self, tx: BaseQueryTxContext):
33+
tx.execute("select 1;")
34+
tx.commit()
35+
36+
def test_interactive_tx_commit(self, tx: BaseQueryTxContext):
37+
tx.execute("select 1;", commit_tx=True)
38+
with pytest.raises(RuntimeError):
39+
tx.execute("select 1;")
40+
41+
def test_tx_execute_raises_after_commit(self, tx: BaseQueryTxContext):
42+
tx.begin()
43+
tx.commit()
44+
with pytest.raises(RuntimeError):
45+
tx.execute("select 1;")
46+
47+
def test_tx_execute_raises_after_rollback(self, tx: BaseQueryTxContext):
48+
tx.begin()
49+
tx.rollback()
50+
with pytest.raises(RuntimeError):
51+
tx.execute("select 1;")
52+
53+
def test_context_manager_rollbacks_tx(self, tx: BaseQueryTxContext):
54+
with tx:
55+
tx.begin()
56+
57+
assert tx._tx_state._state == QueryTxStateEnum.ROLLBACKED
58+
59+
def test_context_manager_normal_flow(self, tx: BaseQueryTxContext):
60+
with tx:
61+
tx.begin()
62+
tx.execute("select 1;")
63+
tx.commit()
64+
65+
assert tx._tx_state._state == QueryTxStateEnum.COMMITTED
66+
67+
def test_context_manager_does_not_hide_exceptions(self, tx: BaseQueryTxContext):
68+
class CustomException(Exception):
69+
pass
70+
71+
with pytest.raises(CustomException):
72+
with tx:
73+
raise CustomException()
74+
75+
def test_execute_as_context_manager(self, tx: BaseQueryTxContext):
76+
tx.begin()
77+
78+
with tx.execute("select 1;") as results:
79+
res = [result_set for result_set in results]
80+
81+
assert len(res) == 1

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ builtins = _
7575
max-line-length = 160
7676
ignore=E203,W503
7777
exclude=*_pb2.py,*_grpc.py,.venv,.git,.tox,dist,doc,*egg,ydb/public/api/protos/*,docs/*,ydb/public/api/grpc/*,persqueue/*,client/*,dbapi/*,ydb/default_pem.py,*docs/conf.py
78+
per-file-ignores = ydb/table.py:F401
7879

7980
[pytest]
8081
asyncio_mode = auto

ydb/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from .tracing import * # noqa
2020
from .topic import * # noqa
2121
from .draft import * # noqa
22+
from .query import * # noqa
23+
from .retries import * # noqa
2224

2325
try:
2426
import ydb.aio as aio # noqa

0 commit comments

Comments
 (0)