atquery 2.11.15__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.
- atquery/__init__.py +47 -0
- atquery/aio/__init__.py +101 -0
- atquery/aio/engine.py +431 -0
- atquery/aio/query.py +391 -0
- atquery/api_v2.py +182 -0
- atquery/atquery2.py +730 -0
- atquery/cli/__init__.py +0 -0
- atquery/cli/columns.py +159 -0
- atquery/cli/ddl.py +151 -0
- atquery/cli/diff.py +196 -0
- atquery/cli/main.py +749 -0
- atquery/cli/migrate.py +312 -0
- atquery/cli/routines.py +142 -0
- atquery/cli/scenario.py +170 -0
- atquery/cli/schemas.py +138 -0
- atquery/cli/tables.py +166 -0
- atquery/dialect/__init__.py +107 -0
- atquery/dialect/duckdb.py +561 -0
- atquery/dialect/mysql/__init__.py +67 -0
- atquery/dialect/mysql/aio.py +166 -0
- atquery/dialect/mysql/core.py +439 -0
- atquery/dialect/mysql/dump/__init__.py +10 -0
- atquery/dialect/mysql/dump/__main__.py +94 -0
- atquery/dialect/mysql/dump/core.py +377 -0
- atquery/dialect/mysql/meta.py +408 -0
- atquery/dialect/mysql/pooling.py +94 -0
- atquery/dialect/mysql/restore/__init__.py +10 -0
- atquery/dialect/mysql/restore/__main__.py +83 -0
- atquery/dialect/mysql/restore/core.py +313 -0
- atquery/dialect/sqlite.py +391 -0
- atquery/diff/__init__.py +1 -0
- atquery/diff/comparator.py +290 -0
- atquery/diff/generator.py +189 -0
- atquery/diff/git_integration.py +93 -0
- atquery/diff/models.py +31 -0
- atquery/diff/parser.py +83 -0
- atquery/engine.py +1011 -0
- atquery/factory.py +140 -0
- atquery/migration/__init__.py +39 -0
- atquery/migration/_migrator.py +163 -0
- atquery/migration/_protocol.py +98 -0
- atquery/migration/_standard.py +250 -0
- atquery/migration/_types.py +48 -0
- atquery/py.typed +0 -0
- atquery/testing/__init__.py +39 -0
- atquery/testing/creator.py +117 -0
- atquery/testing/loader.py +546 -0
- atquery/testing/schema.py +217 -0
- atquery/testing/validator.py +145 -0
- atquery/type.py +126 -0
- atquery-2.11.15.dist-info/METADATA +63 -0
- atquery-2.11.15.dist-info/RECORD +55 -0
- atquery-2.11.15.dist-info/WHEEL +4 -0
- atquery-2.11.15.dist-info/entry_points.txt +3 -0
- atquery-2.11.15.dist-info/licenses/LICENSE +21 -0
atquery/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .api_v2 import (
|
|
2
|
+
NULL,
|
|
3
|
+
ATConnection,
|
|
4
|
+
ATDataSource,
|
|
5
|
+
ATQuery,
|
|
6
|
+
ATResult,
|
|
7
|
+
ATRow,
|
|
8
|
+
ATRowCountError,
|
|
9
|
+
ATSQLError,
|
|
10
|
+
ATString,
|
|
11
|
+
ATWarning,
|
|
12
|
+
Resource,
|
|
13
|
+
create_async_datasource,
|
|
14
|
+
create_datasource,
|
|
15
|
+
escape4like,
|
|
16
|
+
execute,
|
|
17
|
+
execute_script,
|
|
18
|
+
export_csv,
|
|
19
|
+
insert_csv,
|
|
20
|
+
load,
|
|
21
|
+
sql,
|
|
22
|
+
upsert_csv,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"NULL",
|
|
27
|
+
"ATConnection",
|
|
28
|
+
"ATDataSource",
|
|
29
|
+
"ATQuery",
|
|
30
|
+
"ATResult",
|
|
31
|
+
"ATRow",
|
|
32
|
+
"ATRowCountError",
|
|
33
|
+
"ATSQLError",
|
|
34
|
+
"ATString",
|
|
35
|
+
"ATWarning",
|
|
36
|
+
"Resource",
|
|
37
|
+
"create_async_datasource",
|
|
38
|
+
"create_datasource",
|
|
39
|
+
"escape4like",
|
|
40
|
+
"execute",
|
|
41
|
+
"execute_script",
|
|
42
|
+
"insert_csv",
|
|
43
|
+
"load",
|
|
44
|
+
"sql",
|
|
45
|
+
"upsert_csv",
|
|
46
|
+
"export_csv",
|
|
47
|
+
]
|
atquery/aio/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""atquery.aio - async 専用 DataSource / Query の公開 API
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import atquery.aio
|
|
5
|
+
|
|
6
|
+
async_ds = atquery.aio.create_datasource("mysql://user:pass@host/db")
|
|
7
|
+
async_ds = atquery.aio.create_datasource("mysql://user:pass@host/db", pool_size=10)
|
|
8
|
+
|
|
9
|
+
async with async_ds.connect() as conn:
|
|
10
|
+
users = await at.aquery("SELECT__users.sql").getall(conn, {"status": "active"})
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
from importlib import import_module
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import parse_qsl, urlparse
|
|
16
|
+
|
|
17
|
+
from atquery.aio.engine import (
|
|
18
|
+
ATAsyncConnection,
|
|
19
|
+
ATAsyncConnectionPool,
|
|
20
|
+
ATAsyncDataSource,
|
|
21
|
+
ATAsyncResult,
|
|
22
|
+
)
|
|
23
|
+
from atquery.aio.query import ATAsyncQuery, ATAsyncTypedQueryProxy
|
|
24
|
+
|
|
25
|
+
_ASYNC_DATASOURCE_TYPES: dict[str, tuple[str, str]] = {
|
|
26
|
+
"mysql": ("atquery.dialect.mysql.aio", "ATMySQLAsyncDataSource"),
|
|
27
|
+
"mysql+mysqlconnector": ("atquery.dialect.mysql.aio", "ATMySQLAsyncDataSource"),
|
|
28
|
+
"mysql+mysqlconnector#poolable": ("atquery.dialect.mysql.aio", "ATMySQLAsyncPoolableDataSource"),
|
|
29
|
+
"mysql#poolable": ("atquery.dialect.mysql.aio", "ATMySQLAsyncPoolableDataSource"),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_USER = re.compile(r"^.+:\/\/([^:]+:[^@]+@)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_datasource(uri: str, pool_size: int = 0) -> ATAsyncDataSource:
|
|
36
|
+
"""async 専用 DataSource を生成する。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
uri: 接続URI。例: "mysql://user:pass@host/db"
|
|
40
|
+
pool_size: コネクションプールサイズ。0の場合はプールなし。
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
ATAsyncDataSource: async専用DataSource
|
|
44
|
+
"""
|
|
45
|
+
username = None
|
|
46
|
+
password = None
|
|
47
|
+
if match := _USER.match(uri):
|
|
48
|
+
user = match.group(1)
|
|
49
|
+
(username, password) = user.split(":", 1)
|
|
50
|
+
password = password[:-1] # '@' を除く
|
|
51
|
+
uri = uri.replace(user, "", 1)
|
|
52
|
+
|
|
53
|
+
sr = urlparse(uri)
|
|
54
|
+
scheme = sr.scheme
|
|
55
|
+
params: dict[str, Any] = dict(parse_qsl(sr.query))
|
|
56
|
+
|
|
57
|
+
if pool_size:
|
|
58
|
+
scheme += "#poolable"
|
|
59
|
+
params["pool_size"] = pool_size
|
|
60
|
+
|
|
61
|
+
info = _ASYNC_DATASOURCE_TYPES.get(scheme)
|
|
62
|
+
if info is None:
|
|
63
|
+
raise ValueError(f'scheme "{scheme}" は async DataSource でサポートされていません。')
|
|
64
|
+
|
|
65
|
+
cls = getattr(import_module(info[0]), info[1])
|
|
66
|
+
return cls(
|
|
67
|
+
user=username or sr.username,
|
|
68
|
+
password=password or sr.password,
|
|
69
|
+
host=sr.hostname,
|
|
70
|
+
port=sr.port,
|
|
71
|
+
database=sr.path[1:],
|
|
72
|
+
**params,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load(py: str, filepath: str, use_cache: bool = True) -> ATAsyncQuery:
|
|
77
|
+
"""SQLファイルを読み込んで ATAsyncQuery のインスタンスを生成する。
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
py: ファイル検索の起点モジュール(通常は __file__)
|
|
81
|
+
filepath: SQLファイルの相対パス
|
|
82
|
+
use_cache: キャッシュを使用するか
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ATAsyncQuery: インスタンス
|
|
86
|
+
"""
|
|
87
|
+
from atquery.factory import Resource
|
|
88
|
+
|
|
89
|
+
return Resource.aload(py, filepath, use_cache=use_cache)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"ATAsyncConnection",
|
|
94
|
+
"ATAsyncConnectionPool",
|
|
95
|
+
"ATAsyncDataSource",
|
|
96
|
+
"ATAsyncResult",
|
|
97
|
+
"ATAsyncQuery",
|
|
98
|
+
"ATAsyncTypedQueryProxy",
|
|
99
|
+
"create_datasource",
|
|
100
|
+
"load",
|
|
101
|
+
]
|
atquery/aio/engine.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""atquery async engine - DataSource / Connection / Result / ConnectionPool
|
|
2
|
+
|
|
3
|
+
ATAsyncDataSource, ATAsyncConnection, ATAsyncResult, ATAsyncConnectionPool を提供する。
|
|
4
|
+
engine.py に asyncio 依存を持ち込まないため、このモジュールに分離している。
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import collections
|
|
8
|
+
import inspect
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Callable, Generic, Sequence, TypeVar
|
|
11
|
+
|
|
12
|
+
from atquery.engine import (
|
|
13
|
+
ATConnectionPoolTimeoutError,
|
|
14
|
+
ATRow,
|
|
15
|
+
ATSQLError,
|
|
16
|
+
_reject_multi_statement_sql,
|
|
17
|
+
)
|
|
18
|
+
from atquery.type import NULL
|
|
19
|
+
|
|
20
|
+
_pool_log = logging.getLogger("atquery.pool")
|
|
21
|
+
|
|
22
|
+
ConnType = TypeVar("ConnType")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _AsyncConnectContext:
|
|
26
|
+
"""async with / await の両方に対応するコンテキスト。
|
|
27
|
+
|
|
28
|
+
asyncpg と同じパターン:
|
|
29
|
+
async with ds.connect() as conn: ...
|
|
30
|
+
conn = await ds.connect()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, coro) -> None:
|
|
34
|
+
self._coro = coro
|
|
35
|
+
self._conn: "ATAsyncConnection | None" = None
|
|
36
|
+
|
|
37
|
+
def __await__(self):
|
|
38
|
+
return self._coro.__await__()
|
|
39
|
+
|
|
40
|
+
async def __aenter__(self) -> "ATAsyncConnection":
|
|
41
|
+
self._conn = await self._coro
|
|
42
|
+
return self._conn # type: ignore[return-value] # _conn は __aenter__ 後必ず非 None
|
|
43
|
+
|
|
44
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
45
|
+
if self._conn is not None:
|
|
46
|
+
if self._conn.transaction:
|
|
47
|
+
if exc_type is None:
|
|
48
|
+
await self._conn.commit()
|
|
49
|
+
else:
|
|
50
|
+
await self._conn.rollback()
|
|
51
|
+
await self._conn.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ATAsyncResult:
|
|
55
|
+
"""非同期結果セット。非同期カーソルのラッパー。"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
cursor: Any,
|
|
60
|
+
column_names: Sequence[str],
|
|
61
|
+
column_types: Sequence[str | None] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.cursor = cursor
|
|
64
|
+
self.column_names = tuple(column_names)
|
|
65
|
+
self.column_types: tuple[str | None, ...] = (
|
|
66
|
+
tuple(column_types) if column_types else (None,) * len(self.column_names)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __aiter__(self) -> "ATAsyncResult":
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __anext__(self) -> ATRow:
|
|
73
|
+
row = await self.cursor.fetchone()
|
|
74
|
+
if row is None:
|
|
75
|
+
raise StopAsyncIteration
|
|
76
|
+
return ATRow(self.column_names, row)
|
|
77
|
+
|
|
78
|
+
async def __aenter__(self) -> "ATAsyncResult":
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
82
|
+
await self.close()
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return f"{type(self).__name__}({self.column_names})[{self.rowcount}]"
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def rowcount(self) -> int:
|
|
89
|
+
return self.cursor.rowcount
|
|
90
|
+
|
|
91
|
+
async def fetchone(self) -> ATRow | None:
|
|
92
|
+
row = await self.cursor.fetchone()
|
|
93
|
+
if row is None:
|
|
94
|
+
return None
|
|
95
|
+
return ATRow(self.column_names, row)
|
|
96
|
+
|
|
97
|
+
async def fetchall(self) -> list[ATRow]:
|
|
98
|
+
rows = await self.cursor.fetchall()
|
|
99
|
+
return [ATRow(self.column_names, row) for row in rows]
|
|
100
|
+
|
|
101
|
+
async def fetchmany(self, size: int) -> list[ATRow]:
|
|
102
|
+
rows = await self.cursor.fetchmany(size)
|
|
103
|
+
return [ATRow(self.column_names, row) for row in rows]
|
|
104
|
+
|
|
105
|
+
async def scalar(self) -> Any:
|
|
106
|
+
row = await self.fetchone()
|
|
107
|
+
return row[0] if row else None
|
|
108
|
+
|
|
109
|
+
async def close(self) -> None:
|
|
110
|
+
await self.cursor.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ATAsyncConnection(Generic[ConnType]):
|
|
114
|
+
"""非同期接続の基底クラス。"""
|
|
115
|
+
|
|
116
|
+
connection: Any = None
|
|
117
|
+
transaction: bool = False
|
|
118
|
+
|
|
119
|
+
def __init__(self, connection: ConnType) -> None:
|
|
120
|
+
self.connection = connection
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def dialect(self) -> str:
|
|
124
|
+
raise NotImplementedError()
|
|
125
|
+
|
|
126
|
+
async def commit(self) -> None:
|
|
127
|
+
await self.connection.commit()
|
|
128
|
+
|
|
129
|
+
async def rollback(self) -> None:
|
|
130
|
+
await self.connection.rollback()
|
|
131
|
+
|
|
132
|
+
async def close(self) -> None:
|
|
133
|
+
await self.connection.close()
|
|
134
|
+
|
|
135
|
+
async def execute(
|
|
136
|
+
self,
|
|
137
|
+
sql: str,
|
|
138
|
+
params: dict[str, Any] | None = None,
|
|
139
|
+
buffered: bool = False,
|
|
140
|
+
) -> ATAsyncResult:
|
|
141
|
+
if not sql:
|
|
142
|
+
raise ValueError(f'"sql" is required: ${sql}')
|
|
143
|
+
|
|
144
|
+
_reject_multi_statement_sql(sql, params)
|
|
145
|
+
|
|
146
|
+
params = {k: (None if v is NULL else v) for k, v in params.items()} if params else {}
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
return await self.__aexecute__(self.connection, sql, params, buffered)
|
|
150
|
+
except ATSQLError as ex:
|
|
151
|
+
raise ex
|
|
152
|
+
except Exception as ex:
|
|
153
|
+
raise ATSQLError(str(ex), ex, sql=sql, sql_file=None, params=params)
|
|
154
|
+
|
|
155
|
+
async def __aexecute__(
|
|
156
|
+
self, conn: ConnType, sql: str, params: dict, buffered: bool
|
|
157
|
+
) -> ATAsyncResult:
|
|
158
|
+
raise NotImplementedError()
|
|
159
|
+
|
|
160
|
+
async def execute_script(self, script: str) -> None:
|
|
161
|
+
raise NotImplementedError()
|
|
162
|
+
|
|
163
|
+
async def is_connected(self) -> bool:
|
|
164
|
+
try:
|
|
165
|
+
result = await self.execute("SELECT 1")
|
|
166
|
+
await result.fetchone()
|
|
167
|
+
return True
|
|
168
|
+
except Exception:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def _mark_closed_sync(self) -> None:
|
|
172
|
+
"""event loop なしで同期的にコネクションをクローズ済みにマークする。
|
|
173
|
+
|
|
174
|
+
別ループからの dispose() 等で abandon する際に呼び出す。
|
|
175
|
+
SSL transport の ``__del__`` が閉じたループにアクセスしてエラーを起こすのを防ぐ。
|
|
176
|
+
デフォルト実装は何もしない。DBMS 固有の処理はサブクラスでオーバーライドする。
|
|
177
|
+
"""
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ATAsyncDataSource:
|
|
182
|
+
"""async 専用 DataSource の基底クラス。"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
user: str | None = None,
|
|
187
|
+
password: str | None = None,
|
|
188
|
+
host: str | None = None,
|
|
189
|
+
port: int | None = None,
|
|
190
|
+
database: str | None = None,
|
|
191
|
+
**kwargs,
|
|
192
|
+
) -> None:
|
|
193
|
+
self.user = user
|
|
194
|
+
self.password = password
|
|
195
|
+
self.host = host
|
|
196
|
+
self.port = port
|
|
197
|
+
self.database = database
|
|
198
|
+
self.dbconfig = kwargs
|
|
199
|
+
self.warning_handler: Callable | None = None
|
|
200
|
+
self.on_connect: Callable | None = None
|
|
201
|
+
|
|
202
|
+
def __str__(self) -> str:
|
|
203
|
+
port = f":{self.port}" if self.port else ""
|
|
204
|
+
return f"{self.__class__.__name__}({self.user}@{self.host}{port}/{self.database})"
|
|
205
|
+
|
|
206
|
+
async def __aenter__(self) -> "ATAsyncDataSource":
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
210
|
+
await self.dispose()
|
|
211
|
+
|
|
212
|
+
def connect(self) -> _AsyncConnectContext:
|
|
213
|
+
return _AsyncConnectContext(self._do_connect())
|
|
214
|
+
|
|
215
|
+
def begin(self) -> _AsyncConnectContext:
|
|
216
|
+
async def _begin() -> ATAsyncConnection:
|
|
217
|
+
conn = await self._do_connect()
|
|
218
|
+
conn.transaction = True
|
|
219
|
+
return conn
|
|
220
|
+
|
|
221
|
+
return _AsyncConnectContext(_begin())
|
|
222
|
+
|
|
223
|
+
async def _do_connect(self) -> "ATAsyncConnection":
|
|
224
|
+
conn = await self.__aconnect__()
|
|
225
|
+
if self.on_connect is not None:
|
|
226
|
+
if not inspect.iscoroutinefunction(self.on_connect):
|
|
227
|
+
await conn.close()
|
|
228
|
+
raise TypeError("on_connect must be an async function (async def) in ATAsyncDataSource")
|
|
229
|
+
try:
|
|
230
|
+
await self.on_connect(conn)
|
|
231
|
+
except Exception:
|
|
232
|
+
await conn.close()
|
|
233
|
+
raise
|
|
234
|
+
return conn
|
|
235
|
+
|
|
236
|
+
async def __aconnect__(self) -> "ATAsyncConnection":
|
|
237
|
+
raise NotImplementedError()
|
|
238
|
+
|
|
239
|
+
async def dispose(self) -> None:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ATAsyncConnectionPool:
|
|
244
|
+
"""asyncio ベースの非同期コネクションプール。
|
|
245
|
+
|
|
246
|
+
sync の ATConnectionPool と同じ Condition + LIFO deque 方式を使用する。
|
|
247
|
+
asyncio.Condition により、async/await で自然なブロッキングを実現する。
|
|
248
|
+
|
|
249
|
+
- ``max_connections``: idle + active の総数上限
|
|
250
|
+
- ``max_idle_connections``: アイドルキャッシュの上限(LIFO で管理)
|
|
251
|
+
- ``acquire_timeout``: ``acquire()`` の最大待機秒数。``None`` で無限待機
|
|
252
|
+
- ``pool_size``: エイリアス。``max_connections = max_idle_connections = pool_size`` に設定する
|
|
253
|
+
- ``name``: ロガー識別子。複数 DataSource を区別するために使用する(例: ``zeus/my_db``)
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
max_connections: int = 10,
|
|
259
|
+
max_idle_connections: int = 5,
|
|
260
|
+
acquire_timeout: float | None = 30.0,
|
|
261
|
+
*,
|
|
262
|
+
pool_size: int | None = None,
|
|
263
|
+
name: str | None = None,
|
|
264
|
+
) -> None:
|
|
265
|
+
if pool_size is not None:
|
|
266
|
+
max_connections = pool_size
|
|
267
|
+
max_idle_connections = pool_size
|
|
268
|
+
self.max_connections = max_connections
|
|
269
|
+
self.max_idle_connections = max_idle_connections
|
|
270
|
+
self.acquire_timeout = acquire_timeout
|
|
271
|
+
self._idles: collections.deque[ATAsyncConnection] = collections.deque()
|
|
272
|
+
self._active = 0 # idle + checked_out の総数
|
|
273
|
+
self._cond: asyncio.Condition | None = None # 遅延初期化
|
|
274
|
+
self._app_loop: asyncio.AbstractEventLoop | None = None # 接続を作ったループ(初回 acquire 時に記録)
|
|
275
|
+
self._log = logging.getLogger(f"atquery.pool.{name}") if name else _pool_log
|
|
276
|
+
|
|
277
|
+
def _get_condition(self) -> asyncio.Condition:
|
|
278
|
+
if self._cond is None:
|
|
279
|
+
self._cond = asyncio.Condition()
|
|
280
|
+
return self._cond
|
|
281
|
+
|
|
282
|
+
async def acquire(self) -> "ATAsyncConnection | None":
|
|
283
|
+
"""コネクションを取得する。
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
ATAsyncConnection | None: アイドルキャッシュから取得したコネクション、
|
|
287
|
+
または None(呼び出し元が新規作成する)
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ATConnectionPoolTimeoutError: timeout 経過した場合
|
|
291
|
+
"""
|
|
292
|
+
cond = self._get_condition()
|
|
293
|
+
loop = asyncio.get_event_loop()
|
|
294
|
+
if self._app_loop is None:
|
|
295
|
+
self._app_loop = loop # 初回 acquire でアプリのループを記録
|
|
296
|
+
deadline = (
|
|
297
|
+
None if self.acquire_timeout is None else loop.time() + self.acquire_timeout
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async with cond:
|
|
301
|
+
while True:
|
|
302
|
+
# 1. idle を優先して取得(LIFO)
|
|
303
|
+
while self._idles:
|
|
304
|
+
conn = self._idles.pop()
|
|
305
|
+
if await conn.is_connected():
|
|
306
|
+
self._log.debug("[acquire] reuse idle active=%d idles=%d", self._active, len(self._idles))
|
|
307
|
+
return conn
|
|
308
|
+
# 切断済み: 除去してカウント調整
|
|
309
|
+
try:
|
|
310
|
+
await conn.connection.close()
|
|
311
|
+
except Exception:
|
|
312
|
+
pass # 既に破棄済みでも除去は継続する
|
|
313
|
+
self._active -= 1
|
|
314
|
+
self._log.info("[acquire] drop stale active=%d idles=%d", self._active, len(self._idles))
|
|
315
|
+
|
|
316
|
+
# 2. 新規作成できるならスロット確保して返す
|
|
317
|
+
if self._active < self.max_connections:
|
|
318
|
+
self._active += 1
|
|
319
|
+
self._log.debug("[acquire] new slot active=%d idles=%d", self._active, len(self._idles))
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
# 3. 待機
|
|
323
|
+
self._log.info("[acquire] wait active=%d idles=%d max=%d", self._active, len(self._idles), self.max_connections)
|
|
324
|
+
if deadline is not None:
|
|
325
|
+
remaining = deadline - loop.time()
|
|
326
|
+
if remaining <= 0:
|
|
327
|
+
self._log.warning(
|
|
328
|
+
"[acquire] TIMEOUT active=%d idles=%d max=%d tasks=%s",
|
|
329
|
+
self._active, len(self._idles), self.max_connections,
|
|
330
|
+
[t.get_name() for t in asyncio.all_tasks()],
|
|
331
|
+
)
|
|
332
|
+
raise ATConnectionPoolTimeoutError(
|
|
333
|
+
f"Connection pool timed out after {self.acquire_timeout}s "
|
|
334
|
+
f"(max_connections={self.max_connections})"
|
|
335
|
+
)
|
|
336
|
+
try:
|
|
337
|
+
await asyncio.wait_for(cond.wait(), remaining)
|
|
338
|
+
except asyncio.TimeoutError:
|
|
339
|
+
self._log.warning(
|
|
340
|
+
"[acquire] TIMEOUT active=%d idles=%d max=%d tasks=%s",
|
|
341
|
+
self._active, len(self._idles), self.max_connections,
|
|
342
|
+
[t.get_name() for t in asyncio.all_tasks()],
|
|
343
|
+
)
|
|
344
|
+
raise ATConnectionPoolTimeoutError(
|
|
345
|
+
f"Connection pool timed out after {self.acquire_timeout}s "
|
|
346
|
+
f"(max_connections={self.max_connections})"
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
await cond.wait()
|
|
350
|
+
|
|
351
|
+
async def adiscard_slot(self) -> None:
|
|
352
|
+
"""acquire() が None を返した後、接続作成に失敗した場合にスロットを返却する。
|
|
353
|
+
|
|
354
|
+
acquire() は新規接続スロット確保のために _active をインクリメントして None を返す。
|
|
355
|
+
呼び出し元 (DataSource.__aconnect__) で実際の DB 接続が失敗した場合は、
|
|
356
|
+
このメソッドを呼んでスロットを解放しないと _active が過剰カウントされ続け、
|
|
357
|
+
やがてプールが永久にブロックされる。
|
|
358
|
+
"""
|
|
359
|
+
cond = self._get_condition()
|
|
360
|
+
async with cond:
|
|
361
|
+
self._active -= 1
|
|
362
|
+
self._log.info("[discard_slot] active=%d idles=%d", self._active, len(self._idles))
|
|
363
|
+
cond.notify()
|
|
364
|
+
|
|
365
|
+
async def adiscard(self, conn: "ATAsyncConnection") -> None:
|
|
366
|
+
"""コネクションを破棄する。
|
|
367
|
+
|
|
368
|
+
リセット失敗など、プールへの返却が不可能な場合に使用する。
|
|
369
|
+
"""
|
|
370
|
+
cond = self._get_condition()
|
|
371
|
+
async with cond:
|
|
372
|
+
self._active -= 1
|
|
373
|
+
self._log.info("[discard] active=%d idles=%d", self._active, len(self._idles))
|
|
374
|
+
cond.notify()
|
|
375
|
+
try:
|
|
376
|
+
await conn.connection.close()
|
|
377
|
+
except Exception:
|
|
378
|
+
pass # ループが閉じている・既に切断済みの場合は無視
|
|
379
|
+
|
|
380
|
+
async def arelease(self, conn: "ATAsyncConnection") -> None:
|
|
381
|
+
"""コネクションをアイドルキャッシュに返却する。
|
|
382
|
+
|
|
383
|
+
アイドルキャッシュが満杯の場合はコネクションをクローズする。
|
|
384
|
+
いずれの場合もウェイターに通知する。
|
|
385
|
+
"""
|
|
386
|
+
close_conn = False
|
|
387
|
+
cond = self._get_condition()
|
|
388
|
+
async with cond:
|
|
389
|
+
if len(self._idles) < self.max_idle_connections:
|
|
390
|
+
self._idles.append(conn)
|
|
391
|
+
self._log.debug("[release] -> idle active=%d idles=%d", self._active, len(self._idles))
|
|
392
|
+
cond.notify()
|
|
393
|
+
else:
|
|
394
|
+
self._active -= 1
|
|
395
|
+
self._log.debug("[release] -> close active=%d idles=%d", self._active, len(self._idles))
|
|
396
|
+
cond.notify()
|
|
397
|
+
close_conn = True
|
|
398
|
+
if close_conn:
|
|
399
|
+
await conn.connection.close()
|
|
400
|
+
|
|
401
|
+
async def dispose(self) -> None:
|
|
402
|
+
"""全アイドルコネクションをクローズする。
|
|
403
|
+
|
|
404
|
+
呼び出し元のイベントループが接続を作ったループと同一の場合(lifespan shutdown 等)は
|
|
405
|
+
COM_QUIT を送ってグレースフルクローズする。
|
|
406
|
+
別ループから呼ばれた場合(atexit + asyncio.run() 等)は接続を abandon し、
|
|
407
|
+
TCP ソケットの回収は OS に任せる。
|
|
408
|
+
"""
|
|
409
|
+
current_loop = asyncio.get_running_loop()
|
|
410
|
+
same_loop = (self._app_loop is None or self._app_loop is current_loop)
|
|
411
|
+
|
|
412
|
+
conns_to_close: list[ATAsyncConnection] = []
|
|
413
|
+
cond = self._get_condition()
|
|
414
|
+
async with cond:
|
|
415
|
+
while self._idles:
|
|
416
|
+
conn = self._idles.pop()
|
|
417
|
+
self._active -= 1
|
|
418
|
+
if same_loop:
|
|
419
|
+
conns_to_close.append(conn)
|
|
420
|
+
else:
|
|
421
|
+
# 別ループ作成の接続 → abandon
|
|
422
|
+
# SSL transport の __del__ がクローズ済みループを参照してエラーを起こさないよう
|
|
423
|
+
# 同期的にクローズ済みとしてマークする(TCP は OS/MySQL wait_timeout に任せる)
|
|
424
|
+
conn._mark_closed_sync()
|
|
425
|
+
cond.notify_all()
|
|
426
|
+
|
|
427
|
+
for conn in conns_to_close:
|
|
428
|
+
try:
|
|
429
|
+
await conn.connection.close()
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|