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.
Files changed (55) hide show
  1. atquery/__init__.py +47 -0
  2. atquery/aio/__init__.py +101 -0
  3. atquery/aio/engine.py +431 -0
  4. atquery/aio/query.py +391 -0
  5. atquery/api_v2.py +182 -0
  6. atquery/atquery2.py +730 -0
  7. atquery/cli/__init__.py +0 -0
  8. atquery/cli/columns.py +159 -0
  9. atquery/cli/ddl.py +151 -0
  10. atquery/cli/diff.py +196 -0
  11. atquery/cli/main.py +749 -0
  12. atquery/cli/migrate.py +312 -0
  13. atquery/cli/routines.py +142 -0
  14. atquery/cli/scenario.py +170 -0
  15. atquery/cli/schemas.py +138 -0
  16. atquery/cli/tables.py +166 -0
  17. atquery/dialect/__init__.py +107 -0
  18. atquery/dialect/duckdb.py +561 -0
  19. atquery/dialect/mysql/__init__.py +67 -0
  20. atquery/dialect/mysql/aio.py +166 -0
  21. atquery/dialect/mysql/core.py +439 -0
  22. atquery/dialect/mysql/dump/__init__.py +10 -0
  23. atquery/dialect/mysql/dump/__main__.py +94 -0
  24. atquery/dialect/mysql/dump/core.py +377 -0
  25. atquery/dialect/mysql/meta.py +408 -0
  26. atquery/dialect/mysql/pooling.py +94 -0
  27. atquery/dialect/mysql/restore/__init__.py +10 -0
  28. atquery/dialect/mysql/restore/__main__.py +83 -0
  29. atquery/dialect/mysql/restore/core.py +313 -0
  30. atquery/dialect/sqlite.py +391 -0
  31. atquery/diff/__init__.py +1 -0
  32. atquery/diff/comparator.py +290 -0
  33. atquery/diff/generator.py +189 -0
  34. atquery/diff/git_integration.py +93 -0
  35. atquery/diff/models.py +31 -0
  36. atquery/diff/parser.py +83 -0
  37. atquery/engine.py +1011 -0
  38. atquery/factory.py +140 -0
  39. atquery/migration/__init__.py +39 -0
  40. atquery/migration/_migrator.py +163 -0
  41. atquery/migration/_protocol.py +98 -0
  42. atquery/migration/_standard.py +250 -0
  43. atquery/migration/_types.py +48 -0
  44. atquery/py.typed +0 -0
  45. atquery/testing/__init__.py +39 -0
  46. atquery/testing/creator.py +117 -0
  47. atquery/testing/loader.py +546 -0
  48. atquery/testing/schema.py +217 -0
  49. atquery/testing/validator.py +145 -0
  50. atquery/type.py +126 -0
  51. atquery-2.11.15.dist-info/METADATA +63 -0
  52. atquery-2.11.15.dist-info/RECORD +55 -0
  53. atquery-2.11.15.dist-info/WHEEL +4 -0
  54. atquery-2.11.15.dist-info/entry_points.txt +3 -0
  55. 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
+ ]
@@ -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