Skip to content

Commit 13c5985

Browse files
authored
Merge pull request #463 from ydb-platform/typed_parameters
Typed parameters in Query Service
2 parents 792ee57 + 90390c8 commit 13c5985

File tree

8 files changed

+277
-1
lines changed

8 files changed

+277
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2+
* Query service client support
3+
* Add dunder version to ydb package
14
* OAuth 2.0 token exchange. Allow multiple resource parameters in according to https://www.rfc-editor.org/rfc/rfc8693
25

36
## 3.14.0 ##

docker-compose-tls.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ services:
1111
- ./ydb_certs:/ydb_certs
1212
environment:
1313
- YDB_USE_IN_MEMORY_PDISKS=true
14+
- YDB_ENABLE_COLUMN_TABLES=true

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ services:
88
hostname: localhost
99
environment:
1010
- YDB_USE_IN_MEMORY_PDISKS=true
11+
- YDB_ENABLE_COLUMN_TABLES=true

examples/query-service/basic_example.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,61 @@ def callee(session):
8282

8383
pool.retry_operation_sync(callee)
8484

85+
def callee(session: ydb.QuerySessionSync):
86+
query_print = """select $a"""
87+
88+
print("=" * 50)
89+
print("Check implicit typed parameters")
90+
91+
values = [
92+
1,
93+
1.0,
94+
True,
95+
"text",
96+
{"4": 8, "15": 16, "23": 42},
97+
[{"name": "Michael"}, {"surname": "Scott"}],
98+
]
99+
100+
for value in values:
101+
print(f"value: {value}")
102+
with session.transaction().execute(
103+
query=query_print,
104+
parameters={"$a": value},
105+
commit_tx=True,
106+
) as results:
107+
for result_set in results:
108+
print(f"rows: {str(result_set.rows)}")
109+
110+
print("=" * 50)
111+
print("Check typed parameters as tuple pair")
112+
113+
typed_value = ([1, 2, 3], ydb.ListType(ydb.PrimitiveType.Int64))
114+
print(f"value: {typed_value}")
115+
116+
with session.transaction().execute(
117+
query=query_print,
118+
parameters={"$a": typed_value},
119+
commit_tx=True,
120+
) as results:
121+
for result_set in results:
122+
print(f"rows: {str(result_set.rows)}")
123+
124+
print("=" * 50)
125+
print("Check typed parameters as ydb.TypedValue")
126+
127+
typed_value = ydb.TypedValue(111, ydb.PrimitiveType.Int64)
128+
print(f"value: {typed_value}")
129+
130+
with session.transaction().execute(
131+
query=query_print,
132+
parameters={"$a": typed_value},
133+
commit_tx=True,
134+
) as results:
135+
for result_set in results:
136+
print(f"rows: {str(result_set.rows)}")
137+
138+
pool.retry_operation_sync(callee)
139+
85140

86141
if __name__ == "__main__":
87142
main()

tests/query/test_query_parameters.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import pytest
2+
import ydb
3+
4+
5+
query = """SELECT $a AS value"""
6+
7+
8+
def test_select_implicit_int(pool: ydb.QuerySessionPool):
9+
expected_value = 111
10+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
11+
actual_value = res[0].rows[0]["value"]
12+
assert expected_value == actual_value
13+
14+
15+
def test_select_implicit_float(pool: ydb.QuerySessionPool):
16+
expected_value = 11.1
17+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
18+
actual_value = res[0].rows[0]["value"]
19+
assert expected_value == pytest.approx(actual_value)
20+
21+
22+
def test_select_implicit_bool(pool: ydb.QuerySessionPool):
23+
expected_value = False
24+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
25+
actual_value = res[0].rows[0]["value"]
26+
assert expected_value == actual_value
27+
28+
29+
def test_select_implicit_str(pool: ydb.QuerySessionPool):
30+
expected_value = "text"
31+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
32+
actual_value = res[0].rows[0]["value"]
33+
assert expected_value == actual_value
34+
35+
36+
def test_select_implicit_list(pool: ydb.QuerySessionPool):
37+
expected_value = [1, 2, 3]
38+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
39+
actual_value = res[0].rows[0]["value"]
40+
assert expected_value == actual_value
41+
42+
43+
def test_select_implicit_dict(pool: ydb.QuerySessionPool):
44+
expected_value = {"a": 1, "b": 2}
45+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
46+
actual_value = res[0].rows[0]["value"]
47+
assert expected_value == actual_value
48+
49+
50+
def test_select_implicit_list_nested(pool: ydb.QuerySessionPool):
51+
expected_value = [{"a": 1}, {"b": 2}]
52+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
53+
actual_value = res[0].rows[0]["value"]
54+
assert expected_value == actual_value
55+
56+
57+
def test_select_implicit_dict_nested(pool: ydb.QuerySessionPool):
58+
expected_value = {"a": [1, 2, 3], "b": [4, 5]}
59+
res = pool.execute_with_retries(query, parameters={"$a": expected_value})
60+
actual_value = res[0].rows[0]["value"]
61+
assert expected_value == actual_value
62+
63+
64+
def test_select_implicit_custom_type_raises(pool: ydb.QuerySessionPool):
65+
class CustomClass:
66+
pass
67+
68+
expected_value = CustomClass()
69+
with pytest.raises(ValueError):
70+
pool.execute_with_retries(query, parameters={"$a": expected_value})
71+
72+
73+
def test_select_implicit_empty_list_raises(pool: ydb.QuerySessionPool):
74+
expected_value = []
75+
with pytest.raises(ValueError):
76+
pool.execute_with_retries(query, parameters={"$a": expected_value})
77+
78+
79+
def test_select_implicit_empty_dict_raises(pool: ydb.QuerySessionPool):
80+
expected_value = {}
81+
with pytest.raises(ValueError):
82+
pool.execute_with_retries(query, parameters={"$a": expected_value})
83+
84+
85+
def test_select_explicit_primitive(pool: ydb.QuerySessionPool):
86+
expected_value = 111
87+
res = pool.execute_with_retries(query, parameters={"$a": (expected_value, ydb.PrimitiveType.Int64)})
88+
actual_value = res[0].rows[0]["value"]
89+
assert expected_value == actual_value
90+
91+
92+
def test_select_explicit_list(pool: ydb.QuerySessionPool):
93+
expected_value = [1, 2, 3]
94+
type_ = ydb.ListType(ydb.PrimitiveType.Int64)
95+
res = pool.execute_with_retries(query, parameters={"$a": (expected_value, type_)})
96+
actual_value = res[0].rows[0]["value"]
97+
assert expected_value == actual_value
98+
99+
100+
def test_select_explicit_dict(pool: ydb.QuerySessionPool):
101+
expected_value = {"key": "value"}
102+
type_ = ydb.DictType(ydb.PrimitiveType.Utf8, ydb.PrimitiveType.Utf8)
103+
res = pool.execute_with_retries(query, parameters={"$a": (expected_value, type_)})
104+
actual_value = res[0].rows[0]["value"]
105+
assert expected_value == actual_value
106+
107+
108+
def test_select_explicit_empty_list_not_raises(pool: ydb.QuerySessionPool):
109+
expected_value = []
110+
type_ = ydb.ListType(ydb.PrimitiveType.Int64)
111+
res = pool.execute_with_retries(query, parameters={"$a": (expected_value, type_)})
112+
actual_value = res[0].rows[0]["value"]
113+
assert expected_value == actual_value
114+
115+
116+
def test_select_explicit_empty_dict_not_raises(pool: ydb.QuerySessionPool):
117+
expected_value = {}
118+
type_ = ydb.DictType(ydb.PrimitiveType.Utf8, ydb.PrimitiveType.Utf8)
119+
res = pool.execute_with_retries(query, parameters={"$a": (expected_value, type_)})
120+
actual_value = res[0].rows[0]["value"]
121+
assert expected_value == actual_value
122+
123+
124+
def test_select_typedvalue_full_primitive(pool: ydb.QuerySessionPool):
125+
expected_value = 111
126+
typed_value = ydb.TypedValue(expected_value, ydb.PrimitiveType.Int64)
127+
res = pool.execute_with_retries(query, parameters={"$a": typed_value})
128+
actual_value = res[0].rows[0]["value"]
129+
assert expected_value == actual_value
130+
131+
132+
def test_select_typedvalue_implicit_primitive(pool: ydb.QuerySessionPool):
133+
expected_value = 111
134+
typed_value = ydb.TypedValue(expected_value)
135+
res = pool.execute_with_retries(query, parameters={"$a": typed_value})
136+
actual_value = res[0].rows[0]["value"]
137+
assert expected_value == actual_value
138+
139+
140+
def test_select_typevalue_custom_type_raises(pool: ydb.QuerySessionPool):
141+
class CustomClass:
142+
pass
143+
144+
expected_value = CustomClass()
145+
typed_value = ydb.TypedValue(expected_value)
146+
with pytest.raises(ValueError):
147+
pool.execute_with_retries(query, parameters={"$a": typed_value})

ydb/_grpc/grpcwrapper/ydb_query.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
ServerStatus,
1919
)
2020

21+
from ... import convert
22+
2123

2224
@dataclass
2325
class CreateSessionResponse(IFromProto):
@@ -176,5 +178,5 @@ def to_proto(self) -> ydb_query_pb2.ExecuteQueryRequest:
176178
exec_mode=self.exec_mode,
177179
stats_mode=self.stats_mode,
178180
concurrent_result_sets=self.concurrent_result_sets,
179-
parameters=self.parameters,
181+
parameters=convert.query_parameters_to_pb(self.parameters),
180182
)

ydb/convert.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,66 @@ def parameters_to_pb(parameters_types, parameters_values):
281281
return param_values_pb
282282

283283

284+
def query_parameters_to_pb(parameters):
285+
if parameters is None or not parameters:
286+
return {}
287+
288+
parameters_types = {}
289+
parameters_values = {}
290+
for name, value in parameters.items():
291+
if isinstance(value, types.TypedValue):
292+
if value.value_type is None:
293+
value.value_type = _type_from_python_native(value.value)
294+
elif isinstance(value, tuple):
295+
value = types.TypedValue(*value)
296+
else:
297+
value = types.TypedValue(value, _type_from_python_native(value))
298+
299+
parameters_values[name] = value.value
300+
parameters_types[name] = value.value_type
301+
302+
return parameters_to_pb(parameters_types, parameters_values)
303+
304+
305+
_from_python_type_map = {
306+
int: types.PrimitiveType.Int64,
307+
float: types.PrimitiveType.Float,
308+
bool: types.PrimitiveType.Bool,
309+
str: types.PrimitiveType.Utf8,
310+
}
311+
312+
313+
def _type_from_python_native(value):
314+
t = type(value)
315+
316+
if t in _from_python_type_map:
317+
return _from_python_type_map[t]
318+
319+
if t == list:
320+
if len(value) == 0:
321+
raise ValueError(
322+
"Could not map empty list to any type, please specify "
323+
"it manually by tuple(value, type) or ydb.TypedValue"
324+
)
325+
entry_type = _type_from_python_native(value[0])
326+
return types.ListType(entry_type)
327+
328+
if t == dict:
329+
if len(value) == 0:
330+
raise ValueError(
331+
"Could not map empty dict to any type, please specify "
332+
"it manually by tuple(value, type) or ydb.TypedValue"
333+
)
334+
entry = list(value.items())[0]
335+
key_type = _type_from_python_native(entry[0])
336+
value_type = _type_from_python_native(entry[1])
337+
return types.DictType(key_type, value_type)
338+
339+
raise ValueError(
340+
"Could not map value to any type, please specify it manually by tuple(value, type) or ydb.TypedValue"
341+
)
342+
343+
284344
def _unwrap_optionality(column):
285345
c_type = column.type
286346
current_type = c_type.WhichOneof("type")

ydb/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import abc
5+
from dataclasses import dataclass
56
import enum
67
import json
78
from . import _utilities, _apis
@@ -441,3 +442,9 @@ def proto(self):
441442

442443
def __str__(self):
443444
return "BulkUpsertColumns<%s>" % ",".join(self.__columns_repr)
445+
446+
447+
@dataclass
448+
class TypedValue:
449+
value: typing.Any
450+
value_type: typing.Optional[typing.Union[PrimitiveType, AbstractTypeBuilder]] = None

0 commit comments

Comments
 (0)