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.
@@ -0,0 +1,196 @@
1
+ from contextlib import suppress
2
+
3
+ import pytest
4
+ import pytest_asyncio
5
+ import sqlalchemy.util as util
6
+ import ydb
7
+
8
+ import ydb_sqlalchemy.dbapi as dbapi
9
+
10
+
11
+ class BaseDBApiTestSuit:
12
+ def _test_isolation_level_read_only(self, connection: dbapi.Connection, isolation_level: str, read_only: bool):
13
+ connection.cursor().execute(
14
+ dbapi.YdbQuery("CREATE TABLE foo(id Int64 NOT NULL, PRIMARY KEY (id))", is_ddl=True)
15
+ )
16
+ connection.set_isolation_level(isolation_level)
17
+
18
+ cursor = connection.cursor()
19
+
20
+ connection.begin()
21
+
22
+ query = dbapi.YdbQuery("UPSERT INTO foo(id) VALUES (1)")
23
+ if read_only:
24
+ with pytest.raises(dbapi.DatabaseError):
25
+ cursor.execute(query)
26
+ else:
27
+ cursor.execute(query)
28
+
29
+ connection.rollback()
30
+
31
+ connection.cursor().execute(dbapi.YdbQuery("DROP TABLE foo", is_ddl=True))
32
+ connection.cursor().close()
33
+
34
+ def _test_connection(self, connection: dbapi.Connection):
35
+ connection.commit()
36
+ connection.rollback()
37
+
38
+ cur = connection.cursor()
39
+ with suppress(dbapi.DatabaseError):
40
+ cur.execute(dbapi.YdbQuery("DROP TABLE foo", is_ddl=True))
41
+
42
+ assert not connection.check_exists("/local/foo")
43
+ with pytest.raises(dbapi.ProgrammingError):
44
+ connection.describe("/local/foo")
45
+
46
+ cur.execute(dbapi.YdbQuery("CREATE TABLE foo(id Int64 NOT NULL, PRIMARY KEY (id))", is_ddl=True))
47
+
48
+ assert connection.check_exists("/local/foo")
49
+
50
+ col = connection.describe("/local/foo").columns[0]
51
+ assert col.name == "id"
52
+ assert col.type == ydb.PrimitiveType.Int64
53
+
54
+ cur.execute(dbapi.YdbQuery("DROP TABLE foo", is_ddl=True))
55
+ cur.close()
56
+
57
+ def _test_cursor_raw_query(self, connection: dbapi.Connection):
58
+ cur = connection.cursor()
59
+ assert cur
60
+
61
+ with suppress(dbapi.DatabaseError):
62
+ cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
63
+
64
+ cur.execute(dbapi.YdbQuery("CREATE TABLE test(id Int64 NOT NULL, text Utf8, PRIMARY KEY (id))", is_ddl=True))
65
+
66
+ cur.execute(
67
+ dbapi.YdbQuery(
68
+ """
69
+ DECLARE $data AS List<Struct<id:Int64, text: Utf8>>;
70
+
71
+ INSERT INTO test SELECT id, text FROM AS_TABLE($data);
72
+ """,
73
+ parameters_types={
74
+ "$data": ydb.ListType(
75
+ ydb.StructType()
76
+ .add_member("id", ydb.PrimitiveType.Int64)
77
+ .add_member("text", ydb.PrimitiveType.Utf8)
78
+ )
79
+ },
80
+ ),
81
+ {
82
+ "$data": [
83
+ {"id": 17, "text": "seventeen"},
84
+ {"id": 21, "text": "twenty one"},
85
+ ]
86
+ },
87
+ )
88
+
89
+ cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
90
+
91
+ cur.close()
92
+
93
+ def _test_errors(self, connection: dbapi.Connection):
94
+ with pytest.raises(dbapi.InterfaceError):
95
+ dbapi.YdbDBApi().connect("localhost:2136", database="/local666")
96
+
97
+ cur = connection.cursor()
98
+
99
+ with suppress(dbapi.DatabaseError):
100
+ cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
101
+
102
+ with pytest.raises(dbapi.DataError):
103
+ cur.execute(dbapi.YdbQuery("SELECT 18446744073709551616"))
104
+
105
+ with pytest.raises(dbapi.DataError):
106
+ cur.execute(dbapi.YdbQuery("SELECT * FROM 拉屎"))
107
+
108
+ with pytest.raises(dbapi.DataError):
109
+ cur.execute(dbapi.YdbQuery("SELECT floor(5 / 2)"))
110
+
111
+ with pytest.raises(dbapi.ProgrammingError):
112
+ cur.execute(dbapi.YdbQuery("SELECT * FROM test"))
113
+
114
+ cur.execute(dbapi.YdbQuery("CREATE TABLE test(id Int64, PRIMARY KEY (id))", is_ddl=True))
115
+
116
+ cur.execute(dbapi.YdbQuery("INSERT INTO test(id) VALUES(1)"))
117
+ with pytest.raises(dbapi.IntegrityError):
118
+ cur.execute(dbapi.YdbQuery("INSERT INTO test(id) VALUES(1)"))
119
+
120
+ cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
121
+ cur.close()
122
+
123
+
124
+ class TestSyncConnection(BaseDBApiTestSuit):
125
+ @pytest.fixture
126
+ def sync_connection(self) -> dbapi.Connection:
127
+ conn = dbapi.YdbDBApi().connect(host="localhost", port="2136", database="/local")
128
+ try:
129
+ yield conn
130
+ finally:
131
+ conn.close()
132
+
133
+ @pytest.mark.parametrize(
134
+ "isolation_level, read_only",
135
+ [
136
+ (dbapi.IsolationLevel.SERIALIZABLE, False),
137
+ (dbapi.IsolationLevel.AUTOCOMMIT, False),
138
+ (dbapi.IsolationLevel.ONLINE_READONLY, True),
139
+ (dbapi.IsolationLevel.ONLINE_READONLY_INCONSISTENT, True),
140
+ (dbapi.IsolationLevel.STALE_READONLY, True),
141
+ (dbapi.IsolationLevel.SNAPSHOT_READONLY, True),
142
+ ],
143
+ )
144
+ def test_isolation_level_read_only(self, isolation_level: str, read_only: bool, sync_connection: dbapi.Connection):
145
+ self._test_isolation_level_read_only(sync_connection, isolation_level, read_only)
146
+
147
+ def test_connection(self, sync_connection: dbapi.Connection):
148
+ self._test_connection(sync_connection)
149
+
150
+ def test_cursor_raw_query(self, sync_connection: dbapi.Connection):
151
+ return self._test_cursor_raw_query(sync_connection)
152
+
153
+ def test_errors(self, sync_connection: dbapi.Connection):
154
+ return self._test_errors(sync_connection)
155
+
156
+
157
+ class TestAsyncConnection(BaseDBApiTestSuit):
158
+ @pytest_asyncio.fixture
159
+ async def async_connection(self) -> dbapi.AsyncConnection:
160
+ def connect():
161
+ return dbapi.YdbDBApi().async_connect(host="localhost", port="2136", database="/local")
162
+
163
+ conn = await util.greenlet_spawn(connect)
164
+ try:
165
+ yield conn
166
+ finally:
167
+ await util.greenlet_spawn(conn.close)
168
+
169
+ @pytest.mark.asyncio
170
+ @pytest.mark.parametrize(
171
+ "isolation_level, read_only",
172
+ [
173
+ (dbapi.IsolationLevel.SERIALIZABLE, False),
174
+ (dbapi.IsolationLevel.AUTOCOMMIT, False),
175
+ (dbapi.IsolationLevel.ONLINE_READONLY, True),
176
+ (dbapi.IsolationLevel.ONLINE_READONLY_INCONSISTENT, True),
177
+ (dbapi.IsolationLevel.STALE_READONLY, True),
178
+ (dbapi.IsolationLevel.SNAPSHOT_READONLY, True),
179
+ ],
180
+ )
181
+ async def test_isolation_level_read_only(
182
+ self, isolation_level: str, read_only: bool, async_connection: dbapi.AsyncConnection
183
+ ):
184
+ await util.greenlet_spawn(self._test_isolation_level_read_only, async_connection, isolation_level, read_only)
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_connection(self, async_connection: dbapi.AsyncConnection):
188
+ await util.greenlet_spawn(self._test_connection, async_connection)
189
+
190
+ @pytest.mark.asyncio
191
+ async def test_cursor_raw_query(self, async_connection: dbapi.AsyncConnection):
192
+ await util.greenlet_spawn(self._test_cursor_raw_query, async_connection)
193
+
194
+ @pytest.mark.asyncio
195
+ async def test_errors(self, async_connection: dbapi.AsyncConnection):
196
+ await util.greenlet_spawn(self._test_errors, async_connection)
@@ -0,0 +1,3 @@
1
+ from .dbapi import IsolationLevel # noqa: F401
2
+ from .sqlalchemy import Upsert, types, upsert # noqa: F401
3
+ from _version import VERSION
@@ -0,0 +1 @@
1
+ VERSION = "0.0.1b4"
@@ -0,0 +1,43 @@
1
+ from .connection import AsyncConnection, Connection, IsolationLevel # noqa: F401
2
+ from .cursor import AsyncCursor, Cursor, YdbQuery # noqa: F401
3
+ from .errors import (
4
+ DatabaseError,
5
+ DataError,
6
+ Error,
7
+ IntegrityError,
8
+ InterfaceError,
9
+ InternalError,
10
+ NotSupportedError,
11
+ OperationalError,
12
+ ProgrammingError,
13
+ Warning,
14
+ )
15
+
16
+
17
+ class YdbDBApi:
18
+ def __init__(self):
19
+ self.paramstyle = "pyformat"
20
+ self.threadsafety = 0
21
+ self.apilevel = "1.0"
22
+ self._init_dbapi_attributes()
23
+
24
+ def _init_dbapi_attributes(self):
25
+ for name, value in {
26
+ "Warning": Warning,
27
+ "Error": Error,
28
+ "InterfaceError": InterfaceError,
29
+ "DatabaseError": DatabaseError,
30
+ "DataError": DataError,
31
+ "OperationalError": OperationalError,
32
+ "IntegrityError": IntegrityError,
33
+ "InternalError": InternalError,
34
+ "ProgrammingError": ProgrammingError,
35
+ "NotSupportedError": NotSupportedError,
36
+ }.items():
37
+ setattr(self, name, value)
38
+
39
+ def connect(self, *args, **kwargs) -> Connection:
40
+ return Connection(*args, **kwargs)
41
+
42
+ def async_connect(self, *args, **kwargs) -> AsyncConnection:
43
+ return AsyncConnection(*args, **kwargs)
@@ -0,0 +1,180 @@
1
+ import collections.abc
2
+ import posixpath
3
+ from typing import Any, List, NamedTuple, Optional
4
+
5
+ import sqlalchemy.util as util
6
+ import ydb
7
+
8
+ from .cursor import AsyncCursor, Cursor
9
+ from .errors import InterfaceError, InternalError, NotSupportedError
10
+
11
+
12
+ class IsolationLevel:
13
+ SERIALIZABLE = "SERIALIZABLE"
14
+ ONLINE_READONLY = "ONLINE READONLY"
15
+ ONLINE_READONLY_INCONSISTENT = "ONLINE READONLY INCONSISTENT"
16
+ STALE_READONLY = "STALE READONLY"
17
+ SNAPSHOT_READONLY = "SNAPSHOT READONLY"
18
+ AUTOCOMMIT = "AUTOCOMMIT"
19
+
20
+
21
+ class Connection:
22
+ _await = staticmethod(util.await_only)
23
+
24
+ _is_async = False
25
+ _ydb_driver_class = ydb.Driver
26
+ _ydb_session_pool_class = ydb.SessionPool
27
+ _ydb_table_client_class = ydb.TableClient
28
+ _cursor_class = Cursor
29
+
30
+ def __init__(
31
+ self,
32
+ host: str = "",
33
+ port: str = "",
34
+ database: str = "",
35
+ **conn_kwargs: Any,
36
+ ):
37
+ self.endpoint = f"grpc://{host}:{port}"
38
+ self.database = database
39
+ self.conn_kwargs = conn_kwargs
40
+
41
+ if "ydb_session_pool" in self.conn_kwargs: # Use session pool managed manually
42
+ self._shared_session_pool = True
43
+ self.session_pool: ydb.SessionPool = self.conn_kwargs.pop("ydb_session_pool")
44
+ self.driver = (
45
+ self.session_pool._driver
46
+ if hasattr(self.session_pool, "_driver")
47
+ else self.session_pool._pool_impl._driver
48
+ )
49
+ self.driver.table_client = self._ydb_table_client_class(self.driver, self._get_table_client_settings())
50
+ else:
51
+ self._shared_session_pool = False
52
+ self.driver = self._create_driver()
53
+ self.session_pool = self._ydb_session_pool_class(self.driver, size=5)
54
+
55
+ self.interactive_transaction: bool = False # AUTOCOMMIT
56
+ self.tx_mode: ydb.AbstractTransactionModeBuilder = ydb.SerializableReadWrite()
57
+ self.tx_context: Optional[ydb.TxContext] = None
58
+
59
+ def cursor(self):
60
+ return self._cursor_class(self.session_pool, self.tx_mode, self.tx_context)
61
+
62
+ def describe(self, table_path: str) -> ydb.TableDescription:
63
+ abs_table_path = posixpath.join(self.database, table_path)
64
+ cursor = self.cursor()
65
+ return cursor.describe_table(abs_table_path)
66
+
67
+ def check_exists(self, table_path: str) -> ydb.SchemeEntry:
68
+ cursor = self.cursor()
69
+ return cursor.check_exists(table_path)
70
+
71
+ def get_table_names(self) -> List[str]:
72
+ cursor = self.cursor()
73
+ return cursor.get_table_names()
74
+
75
+ def set_isolation_level(self, isolation_level: str):
76
+ class IsolationSettings(NamedTuple):
77
+ ydb_mode: ydb.AbstractTransactionModeBuilder
78
+ interactive: bool
79
+
80
+ ydb_isolation_settings_map = {
81
+ IsolationLevel.AUTOCOMMIT: IsolationSettings(ydb.SerializableReadWrite(), interactive=False),
82
+ IsolationLevel.SERIALIZABLE: IsolationSettings(ydb.SerializableReadWrite(), interactive=True),
83
+ IsolationLevel.ONLINE_READONLY: IsolationSettings(ydb.OnlineReadOnly(), interactive=False),
84
+ IsolationLevel.ONLINE_READONLY_INCONSISTENT: IsolationSettings(
85
+ ydb.OnlineReadOnly().with_allow_inconsistent_reads(), interactive=False
86
+ ),
87
+ IsolationLevel.STALE_READONLY: IsolationSettings(ydb.StaleReadOnly(), interactive=False),
88
+ IsolationLevel.SNAPSHOT_READONLY: IsolationSettings(ydb.SnapshotReadOnly(), interactive=True),
89
+ }
90
+ ydb_isolation_settings = ydb_isolation_settings_map[isolation_level]
91
+ if self.tx_context and self.tx_context.tx_id:
92
+ raise InternalError("Failed to set transaction mode: transaction is already began")
93
+ self.tx_mode = ydb_isolation_settings.ydb_mode
94
+ self.interactive_transaction = ydb_isolation_settings.interactive
95
+
96
+ def get_isolation_level(self) -> str:
97
+ if self.tx_mode.name == ydb.SerializableReadWrite().name:
98
+ if self.interactive_transaction:
99
+ return IsolationLevel.SERIALIZABLE
100
+ else:
101
+ return IsolationLevel.AUTOCOMMIT
102
+ elif self.tx_mode.name == ydb.OnlineReadOnly().name:
103
+ if self.tx_mode.settings.allow_inconsistent_reads:
104
+ return IsolationLevel.ONLINE_READONLY_INCONSISTENT
105
+ else:
106
+ return IsolationLevel.ONLINE_READONLY
107
+ elif self.tx_mode.name == ydb.StaleReadOnly().name:
108
+ return IsolationLevel.STALE_READONLY
109
+ elif self.tx_mode.name == ydb.SnapshotReadOnly().name:
110
+ return IsolationLevel.SNAPSHOT_READONLY
111
+ else:
112
+ raise NotSupportedError(f"{self.tx_mode.name} is not supported")
113
+
114
+ def begin(self):
115
+ self.tx_context = None
116
+ if self.interactive_transaction:
117
+ session = self._maybe_await(self.session_pool.acquire)
118
+ self.tx_context = session.transaction(self.tx_mode)
119
+ self._maybe_await(self.tx_context.begin)
120
+
121
+ def commit(self):
122
+ if self.tx_context and self.tx_context.tx_id:
123
+ self._maybe_await(self.tx_context.commit)
124
+ self._maybe_await(self.session_pool.release, self.tx_context.session)
125
+ self.tx_context = None
126
+
127
+ def rollback(self):
128
+ if self.tx_context and self.tx_context.tx_id:
129
+ self._maybe_await(self.tx_context.rollback)
130
+ self._maybe_await(self.session_pool.release, self.tx_context.session)
131
+ self.tx_context = None
132
+
133
+ def close(self):
134
+ self.rollback()
135
+ if not self._shared_session_pool:
136
+ self._maybe_await(self.session_pool.stop)
137
+ self._stop_driver()
138
+
139
+ @classmethod
140
+ def _maybe_await(cls, callee: collections.abc.Callable, *args, **kwargs) -> Any:
141
+ if cls._is_async:
142
+ return cls._await(callee(*args, **kwargs))
143
+ return callee(*args, **kwargs)
144
+
145
+ def _get_table_client_settings(self) -> ydb.TableClientSettings:
146
+ return (
147
+ ydb.TableClientSettings()
148
+ .with_native_date_in_result_sets(True)
149
+ .with_native_datetime_in_result_sets(True)
150
+ .with_native_timestamp_in_result_sets(True)
151
+ .with_native_interval_in_result_sets(True)
152
+ .with_native_json_in_result_sets(False)
153
+ )
154
+
155
+ def _create_driver(self):
156
+ driver_config = ydb.DriverConfig(
157
+ endpoint=self.endpoint,
158
+ database=self.database,
159
+ table_client_settings=self._get_table_client_settings(),
160
+ )
161
+ driver = self._ydb_driver_class(driver_config)
162
+ try:
163
+ self._maybe_await(driver.wait, timeout=5, fail_fast=True)
164
+ except ydb.Error as e:
165
+ raise InterfaceError(e.message, original_error=e) from e
166
+ except Exception as e:
167
+ self._maybe_await(driver.stop)
168
+ raise InterfaceError(f"Failed to connect to YDB, details {driver.discovery_debug_details()}") from e
169
+ return driver
170
+
171
+ def _stop_driver(self):
172
+ self._maybe_await(self.driver.stop)
173
+
174
+
175
+ class AsyncConnection(Connection):
176
+ _is_async = True
177
+ _ydb_driver_class = ydb.aio.Driver
178
+ _ydb_session_pool_class = ydb.aio.SessionPool
179
+ _ydb_table_client_class = ydb.aio.table.TableClient
180
+ _cursor_class = AsyncCursor
@@ -0,0 +1,218 @@
1
+ YDB_KEYWORDS = {
2
+ "abort",
3
+ "action",
4
+ "add",
5
+ "after",
6
+ "all",
7
+ "alter",
8
+ "analyze",
9
+ "and",
10
+ "ansi",
11
+ "any",
12
+ "array",
13
+ "as",
14
+ "asc",
15
+ "assume",
16
+ "async",
17
+ "attach",
18
+ "autoincrement",
19
+ "before",
20
+ "begin",
21
+ "bernoulli",
22
+ "between",
23
+ "bitcast",
24
+ "by",
25
+ "cascade",
26
+ "case",
27
+ "cast",
28
+ "changefeed",
29
+ "check",
30
+ "collate",
31
+ "column",
32
+ "columns",
33
+ "commit",
34
+ "compact",
35
+ "conditional",
36
+ "conflict",
37
+ "constraint",
38
+ "consumer",
39
+ "cover",
40
+ "create",
41
+ "cross",
42
+ "cube",
43
+ "current",
44
+ "current_date",
45
+ "current_time",
46
+ "current_timestamp",
47
+ "data",
48
+ "database",
49
+ "decimal",
50
+ "declare",
51
+ "default",
52
+ "deferrable",
53
+ "deferred",
54
+ "define",
55
+ "delete",
56
+ "desc",
57
+ "detach",
58
+ "disable",
59
+ "discard",
60
+ "distinct",
61
+ "do",
62
+ "drop",
63
+ "each",
64
+ "else",
65
+ "empty",
66
+ "empty_action",
67
+ "encrypted",
68
+ "end",
69
+ "erase",
70
+ "error",
71
+ "escape",
72
+ "evaluate",
73
+ "except",
74
+ "exclude",
75
+ "exclusion",
76
+ "exclusive",
77
+ "exists",
78
+ "explain",
79
+ "export",
80
+ "external",
81
+ "fail",
82
+ "family",
83
+ "filter",
84
+ "flatten",
85
+ "following",
86
+ "for",
87
+ "foreign",
88
+ "from",
89
+ "full",
90
+ "function",
91
+ "glob",
92
+ "group",
93
+ "grouping",
94
+ "groups",
95
+ "hash",
96
+ "having",
97
+ "hop",
98
+ "if",
99
+ "ignore",
100
+ "ilike",
101
+ "immediate",
102
+ "import",
103
+ "in",
104
+ "index",
105
+ "indexed",
106
+ "inherits",
107
+ "initially",
108
+ "inner",
109
+ "insert",
110
+ "instead",
111
+ "intersect",
112
+ "into",
113
+ "is",
114
+ "isnull",
115
+ "join",
116
+ "json_exists",
117
+ "json_query",
118
+ "json_value",
119
+ "key",
120
+ "left",
121
+ "like",
122
+ "limit",
123
+ "local",
124
+ "match",
125
+ "natural",
126
+ "no",
127
+ "not",
128
+ "notnull",
129
+ "null",
130
+ "nulls",
131
+ "object",
132
+ "of",
133
+ "offset",
134
+ "on",
135
+ "only",
136
+ "or",
137
+ "order",
138
+ "others",
139
+ "outer",
140
+ "over",
141
+ "partition",
142
+ "passing",
143
+ "password",
144
+ "plan",
145
+ "pragma",
146
+ "preceding",
147
+ "presort",
148
+ "primary",
149
+ "process",
150
+ "raise",
151
+ "range",
152
+ "reduce",
153
+ "references",
154
+ "regexp",
155
+ "reindex",
156
+ "release",
157
+ "rename",
158
+ "replace",
159
+ "replication",
160
+ "reset",
161
+ "respect",
162
+ "restrict",
163
+ "result",
164
+ "return",
165
+ "returning",
166
+ "revert",
167
+ "right",
168
+ "rlike",
169
+ "rollback",
170
+ "rollup",
171
+ "row",
172
+ "rows",
173
+ "sample",
174
+ "savepoint",
175
+ "schema",
176
+ "select",
177
+ "semi",
178
+ "sets",
179
+ "source",
180
+ "stream",
181
+ "subquery",
182
+ "symbols",
183
+ "sync",
184
+ "system",
185
+ "table",
186
+ "tablesample",
187
+ "tablestore",
188
+ "temp",
189
+ "temporary",
190
+ "then",
191
+ "ties",
192
+ "to",
193
+ "topic",
194
+ "transaction",
195
+ "trigger",
196
+ "type",
197
+ "unbounded",
198
+ "unconditional",
199
+ "union",
200
+ "unique",
201
+ "unknown",
202
+ "update",
203
+ "upsert",
204
+ "use",
205
+ "user",
206
+ "using",
207
+ "vacuum",
208
+ "values",
209
+ "view",
210
+ "virtual",
211
+ "when",
212
+ "where",
213
+ "window",
214
+ "with",
215
+ "without",
216
+ "wrapper",
217
+ "xor",
218
+ }