ydb-sqlalchemy 0.0.1b4__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- test/__init__.py +0 -0
- test/conftest.py +11 -0
- test/test_core.py +734 -0
- test/test_inspect.py +34 -0
- test/test_suite.py +553 -0
- test_dbapi/__init__.py +0 -0
- test_dbapi/test_dbapi.py +196 -0
- ydb_sqlalchemy/__init__.py +3 -0
- ydb_sqlalchemy/_version.py +1 -0
- ydb_sqlalchemy/dbapi/__init__.py +43 -0
- ydb_sqlalchemy/dbapi/connection.py +180 -0
- ydb_sqlalchemy/dbapi/constants.py +218 -0
- ydb_sqlalchemy/dbapi/cursor.py +304 -0
- ydb_sqlalchemy/dbapi/errors.py +103 -0
- ydb_sqlalchemy/sqlalchemy/__init__.py +754 -0
- ydb_sqlalchemy/sqlalchemy/dml.py +12 -0
- ydb_sqlalchemy/sqlalchemy/json.py +34 -0
- ydb_sqlalchemy/sqlalchemy/requirements.py +105 -0
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +27 -0
- ydb_sqlalchemy/sqlalchemy/types.py +67 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/LICENSE +201 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/METADATA +89 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/RECORD +26 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/WHEEL +6 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/entry_points.txt +6 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import dataclasses
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Union
|
|
7
|
+
|
|
8
|
+
import ydb
|
|
9
|
+
import ydb.aio
|
|
10
|
+
from sqlalchemy import util
|
|
11
|
+
|
|
12
|
+
from .errors import (
|
|
13
|
+
DatabaseError,
|
|
14
|
+
DataError,
|
|
15
|
+
IntegrityError,
|
|
16
|
+
InternalError,
|
|
17
|
+
NotSupportedError,
|
|
18
|
+
OperationalError,
|
|
19
|
+
ProgrammingError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_column_type(type_obj: Any) -> str:
|
|
26
|
+
return str(ydb.convert.type_to_native(type_obj))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass
|
|
30
|
+
class YdbQuery:
|
|
31
|
+
yql_text: str
|
|
32
|
+
parameters_types: Dict[str, Union[ydb.PrimitiveType, ydb.AbstractTypeBuilder]] = dataclasses.field(
|
|
33
|
+
default_factory=dict
|
|
34
|
+
)
|
|
35
|
+
is_ddl: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle_ydb_errors(func):
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
def wrapper(*args, **kwargs):
|
|
41
|
+
try:
|
|
42
|
+
return func(*args, **kwargs)
|
|
43
|
+
except (ydb.issues.AlreadyExists, ydb.issues.PreconditionFailed) as e:
|
|
44
|
+
raise IntegrityError(e.message, original_error=e) from e
|
|
45
|
+
except (ydb.issues.Unsupported, ydb.issues.Unimplemented) as e:
|
|
46
|
+
raise NotSupportedError(e.message, original_error=e) from e
|
|
47
|
+
except (ydb.issues.BadRequest, ydb.issues.SchemeError) as e:
|
|
48
|
+
raise ProgrammingError(e.message, original_error=e) from e
|
|
49
|
+
except (
|
|
50
|
+
ydb.issues.TruncatedResponseError,
|
|
51
|
+
ydb.issues.ConnectionError,
|
|
52
|
+
ydb.issues.Aborted,
|
|
53
|
+
ydb.issues.Unavailable,
|
|
54
|
+
ydb.issues.Overloaded,
|
|
55
|
+
ydb.issues.Undetermined,
|
|
56
|
+
ydb.issues.Timeout,
|
|
57
|
+
ydb.issues.Cancelled,
|
|
58
|
+
ydb.issues.SessionBusy,
|
|
59
|
+
ydb.issues.SessionExpired,
|
|
60
|
+
ydb.issues.SessionPoolEmpty,
|
|
61
|
+
) as e:
|
|
62
|
+
raise OperationalError(e.message, original_error=e) from e
|
|
63
|
+
except ydb.issues.GenericError as e:
|
|
64
|
+
raise DataError(e.message, original_error=e) from e
|
|
65
|
+
except ydb.issues.InternalError as e:
|
|
66
|
+
raise InternalError(e.message, original_error=e) from e
|
|
67
|
+
except ydb.Error as e:
|
|
68
|
+
raise DatabaseError(e.message, original_error=e) from e
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise DatabaseError("Failed to execute query") from e
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Cursor:
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
session_pool: Union[ydb.SessionPool, ydb.aio.SessionPool],
|
|
79
|
+
tx_mode: ydb.AbstractTransactionModeBuilder,
|
|
80
|
+
tx_context: Optional[ydb.BaseTxContext] = None,
|
|
81
|
+
):
|
|
82
|
+
self.session_pool = session_pool
|
|
83
|
+
self.tx_mode = tx_mode
|
|
84
|
+
self.tx_context = tx_context
|
|
85
|
+
self.description = None
|
|
86
|
+
self.arraysize = 1
|
|
87
|
+
self.rows = None
|
|
88
|
+
self._rows_prefetched = None
|
|
89
|
+
|
|
90
|
+
@_handle_ydb_errors
|
|
91
|
+
def describe_table(self, abs_table_path: str) -> ydb.TableDescription:
|
|
92
|
+
return self._retry_operation_in_pool(self._describe_table, abs_table_path)
|
|
93
|
+
|
|
94
|
+
def check_exists(self, table_path: str) -> bool:
|
|
95
|
+
try:
|
|
96
|
+
self._retry_operation_in_pool(self._describe_path, table_path)
|
|
97
|
+
return True
|
|
98
|
+
except ydb.SchemeError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
@_handle_ydb_errors
|
|
102
|
+
def get_table_names(self) -> List[str]:
|
|
103
|
+
directory: ydb.Directory = self._retry_operation_in_pool(self._list_directory)
|
|
104
|
+
return [child.name for child in directory.children if child.is_table()]
|
|
105
|
+
|
|
106
|
+
def execute(self, operation: YdbQuery, parameters: Optional[Mapping[str, Any]] = None):
|
|
107
|
+
if operation.is_ddl or not operation.parameters_types:
|
|
108
|
+
query = operation.yql_text
|
|
109
|
+
is_ddl = operation.is_ddl
|
|
110
|
+
else:
|
|
111
|
+
query = ydb.DataQuery(operation.yql_text, operation.parameters_types)
|
|
112
|
+
is_ddl = operation.is_ddl
|
|
113
|
+
|
|
114
|
+
logger.info("execute sql: %s, params: %s", query, parameters)
|
|
115
|
+
if is_ddl:
|
|
116
|
+
chunks = self._execute_ddl(query)
|
|
117
|
+
else:
|
|
118
|
+
chunks = self._execute_dml(query, parameters)
|
|
119
|
+
|
|
120
|
+
rows = self._rows_iterable(chunks)
|
|
121
|
+
# Prefetch the description:
|
|
122
|
+
try:
|
|
123
|
+
first_row = next(rows)
|
|
124
|
+
except StopIteration:
|
|
125
|
+
pass
|
|
126
|
+
else:
|
|
127
|
+
rows = itertools.chain((first_row,), rows)
|
|
128
|
+
if self.rows is not None:
|
|
129
|
+
rows = itertools.chain(self.rows, rows)
|
|
130
|
+
|
|
131
|
+
self.rows = rows
|
|
132
|
+
|
|
133
|
+
@_handle_ydb_errors
|
|
134
|
+
def _execute_dml(
|
|
135
|
+
self, query: Union[ydb.DataQuery, str], parameters: Optional[Mapping[str, Any]] = None
|
|
136
|
+
) -> ydb.convert.ResultSets:
|
|
137
|
+
prepared_query = query
|
|
138
|
+
if isinstance(query, str) and parameters:
|
|
139
|
+
if self.tx_context:
|
|
140
|
+
prepared_query = self._run_operation_in_session(self._prepare, query)
|
|
141
|
+
else:
|
|
142
|
+
prepared_query = self._retry_operation_in_pool(self._prepare, query)
|
|
143
|
+
|
|
144
|
+
if self.tx_context:
|
|
145
|
+
return self._run_operation_in_tx(self._execute_in_tx, prepared_query, parameters)
|
|
146
|
+
|
|
147
|
+
return self._retry_operation_in_pool(self._execute_in_session, self.tx_mode, prepared_query, parameters)
|
|
148
|
+
|
|
149
|
+
@_handle_ydb_errors
|
|
150
|
+
def _execute_ddl(self, query: str) -> ydb.convert.ResultSets:
|
|
151
|
+
return self._retry_operation_in_pool(self._execute_scheme, query)
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _execute_scheme(session: ydb.Session, query: str) -> ydb.convert.ResultSets:
|
|
155
|
+
return session.execute_scheme(query)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _describe_table(session: ydb.Session, abs_table_path: str) -> ydb.TableDescription:
|
|
159
|
+
return session.describe_table(abs_table_path)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _describe_path(session: ydb.Session, table_path: str) -> ydb.SchemeEntry:
|
|
163
|
+
return session._driver.scheme_client.describe_path(table_path)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _list_directory(session: ydb.Session) -> ydb.Directory:
|
|
167
|
+
return session._driver.scheme_client.list_directory(session._driver._driver_config.database)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _prepare(session: ydb.Session, query: str) -> ydb.DataQuery:
|
|
171
|
+
return session.prepare(query)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _execute_in_tx(
|
|
175
|
+
tx_context: ydb.TxContext, prepared_query: ydb.DataQuery, parameters: Optional[Mapping[str, Any]]
|
|
176
|
+
) -> ydb.convert.ResultSets:
|
|
177
|
+
return tx_context.execute(prepared_query, parameters, commit_tx=False)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _execute_in_session(
|
|
181
|
+
session: ydb.Session,
|
|
182
|
+
tx_mode: ydb.AbstractTransactionModeBuilder,
|
|
183
|
+
prepared_query: ydb.DataQuery,
|
|
184
|
+
parameters: Optional[Mapping[str, Any]],
|
|
185
|
+
) -> ydb.convert.ResultSets:
|
|
186
|
+
return session.transaction(tx_mode).execute(prepared_query, parameters, commit_tx=True)
|
|
187
|
+
|
|
188
|
+
def _run_operation_in_tx(self, callee: collections.abc.Callable, *args, **kwargs):
|
|
189
|
+
return callee(self.tx_context, *args, **kwargs)
|
|
190
|
+
|
|
191
|
+
def _run_operation_in_session(self, callee: collections.abc.Callable, *args, **kwargs):
|
|
192
|
+
return callee(self.tx_context.session, *args, **kwargs)
|
|
193
|
+
|
|
194
|
+
def _retry_operation_in_pool(self, callee: collections.abc.Callable, *args, **kwargs):
|
|
195
|
+
return self.session_pool.retry_operation_sync(callee, None, *args, **kwargs)
|
|
196
|
+
|
|
197
|
+
def _rows_iterable(self, chunks_iterable: ydb.convert.ResultSets):
|
|
198
|
+
try:
|
|
199
|
+
for chunk in chunks_iterable:
|
|
200
|
+
self.description = [
|
|
201
|
+
(
|
|
202
|
+
col.name,
|
|
203
|
+
get_column_type(col.type),
|
|
204
|
+
None,
|
|
205
|
+
None,
|
|
206
|
+
None,
|
|
207
|
+
None,
|
|
208
|
+
None,
|
|
209
|
+
)
|
|
210
|
+
for col in chunk.columns
|
|
211
|
+
]
|
|
212
|
+
for row in chunk.rows:
|
|
213
|
+
# returns tuple to be compatible with SqlAlchemy and because
|
|
214
|
+
# of this PEP to return a sequence: https://www.python.org/dev/peps/pep-0249/#fetchmany
|
|
215
|
+
yield row[::]
|
|
216
|
+
except ydb.Error as e:
|
|
217
|
+
raise DatabaseError(e.message, original_error=e) from e
|
|
218
|
+
|
|
219
|
+
def _ensure_prefetched(self):
|
|
220
|
+
if self.rows is not None and self._rows_prefetched is None:
|
|
221
|
+
self._rows_prefetched = list(self.rows)
|
|
222
|
+
self.rows = iter(self._rows_prefetched)
|
|
223
|
+
return self._rows_prefetched
|
|
224
|
+
|
|
225
|
+
def executemany(self, operation: YdbQuery, seq_of_parameters: Optional[Sequence[Mapping[str, Any]]]):
|
|
226
|
+
for parameters in seq_of_parameters:
|
|
227
|
+
self.execute(operation, parameters)
|
|
228
|
+
|
|
229
|
+
def executescript(self, script):
|
|
230
|
+
return self.execute(script)
|
|
231
|
+
|
|
232
|
+
def fetchone(self):
|
|
233
|
+
return next(self.rows or [], None)
|
|
234
|
+
|
|
235
|
+
def fetchmany(self, size=None):
|
|
236
|
+
return list(itertools.islice(self.rows, size or self.arraysize))
|
|
237
|
+
|
|
238
|
+
def fetchall(self):
|
|
239
|
+
return list(self.rows)
|
|
240
|
+
|
|
241
|
+
def nextset(self):
|
|
242
|
+
self.fetchall()
|
|
243
|
+
|
|
244
|
+
def setinputsizes(self, sizes):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
def setoutputsize(self, column=None):
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def close(self):
|
|
251
|
+
self.rows = None
|
|
252
|
+
self._rows_prefetched = None
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def rowcount(self):
|
|
256
|
+
return len(self._ensure_prefetched())
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AsyncCursor(Cursor):
|
|
260
|
+
_await = staticmethod(util.await_only)
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
async def _describe_table(session: ydb.aio.table.Session, abs_table_path: str) -> ydb.TableDescription:
|
|
264
|
+
return await session.describe_table(abs_table_path)
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
async def _describe_path(session: ydb.aio.table.Session, table_path: str) -> ydb.SchemeEntry:
|
|
268
|
+
return await session._driver.scheme_client.describe_path(table_path)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
async def _list_directory(session: ydb.aio.table.Session) -> ydb.Directory:
|
|
272
|
+
return await session._driver.scheme_client.list_directory(session._driver._driver_config.database)
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
async def _execute_scheme(session: ydb.aio.table.Session, query: str) -> ydb.convert.ResultSets:
|
|
276
|
+
return await session.execute_scheme(query)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
async def _prepare(session: ydb.aio.table.Session, query: str) -> ydb.DataQuery:
|
|
280
|
+
return await session.prepare(query)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
async def _execute_in_tx(
|
|
284
|
+
tx_context: ydb.aio.table.TxContext, prepared_query: ydb.DataQuery, parameters: Optional[Mapping[str, Any]]
|
|
285
|
+
) -> ydb.convert.ResultSets:
|
|
286
|
+
return await tx_context.execute(prepared_query, parameters, commit_tx=False)
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
async def _execute_in_session(
|
|
290
|
+
session: ydb.aio.table.Session,
|
|
291
|
+
tx_mode: ydb.AbstractTransactionModeBuilder,
|
|
292
|
+
prepared_query: ydb.DataQuery,
|
|
293
|
+
parameters: Optional[Mapping[str, Any]],
|
|
294
|
+
) -> ydb.convert.ResultSets:
|
|
295
|
+
return await session.transaction(tx_mode).execute(prepared_query, parameters, commit_tx=True)
|
|
296
|
+
|
|
297
|
+
def _run_operation_in_tx(self, callee: collections.abc.Coroutine, *args, **kwargs):
|
|
298
|
+
return self._await(callee(self.tx_context, *args, **kwargs))
|
|
299
|
+
|
|
300
|
+
def _run_operation_in_session(self, callee: collections.abc.Coroutine, *args, **kwargs):
|
|
301
|
+
return self._await(callee(self.tx_context.session, *args, **kwargs))
|
|
302
|
+
|
|
303
|
+
def _retry_operation_in_pool(self, callee: collections.abc.Coroutine, *args, **kwargs):
|
|
304
|
+
return self._await(self.session_pool.retry_operation(callee, *args, **kwargs))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import ydb
|
|
4
|
+
from google.protobuf.message import Message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Warning(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Error(Exception):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
original_error: Optional[ydb.Error] = None,
|
|
16
|
+
):
|
|
17
|
+
super(Error, self).__init__(message)
|
|
18
|
+
|
|
19
|
+
self.original_error = original_error
|
|
20
|
+
if original_error:
|
|
21
|
+
pretty_issues = _pretty_issues(original_error.issues)
|
|
22
|
+
self.issues = original_error.issues
|
|
23
|
+
self.message = pretty_issues or message
|
|
24
|
+
self.status = original_error.status
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InterfaceError(Error):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DatabaseError(Error):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DataError(DatabaseError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OperationalError(DatabaseError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IntegrityError(DatabaseError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InternalError(DatabaseError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProgrammingError(DatabaseError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NotSupportedError(DatabaseError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _pretty_issues(issues: List[Message]) -> str:
|
|
60
|
+
if issues is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
children_messages = [_get_messages(issue, root=True) for issue in issues]
|
|
64
|
+
|
|
65
|
+
if None in children_messages:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return "\n" + "\n".join(children_messages)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_messages(issue: Message, max_depth: int = 100, indent: int = 2, depth: int = 0, root: bool = False) -> str:
|
|
72
|
+
if depth >= max_depth:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
margin_str = " " * depth * indent
|
|
76
|
+
pre_message = ""
|
|
77
|
+
children = ""
|
|
78
|
+
|
|
79
|
+
if issue.issues:
|
|
80
|
+
collapsed_messages = []
|
|
81
|
+
while not root and len(issue.issues) == 1:
|
|
82
|
+
collapsed_messages.append(issue.message)
|
|
83
|
+
issue = issue.issues[0]
|
|
84
|
+
|
|
85
|
+
if collapsed_messages:
|
|
86
|
+
pre_message = f"{margin_str}{', '.join(collapsed_messages)}\n"
|
|
87
|
+
depth += 1
|
|
88
|
+
margin_str = " " * depth * indent
|
|
89
|
+
|
|
90
|
+
children_messages = [
|
|
91
|
+
_get_messages(iss, max_depth=max_depth, indent=indent, depth=depth + 1) for iss in issue.issues
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
if None in children_messages:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
children = "\n".join(children_messages)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
f"{pre_message}{margin_str}{issue.message}\n{margin_str}"
|
|
101
|
+
f"severity level: {issue.severity}\n{margin_str}"
|
|
102
|
+
f"issue code: {issue.issue_code}\n{children}"
|
|
103
|
+
)
|