nanasqlite 1.3.3.dev4__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.
- nanasqlite/__init__.py +52 -0
- nanasqlite/async_core.py +1456 -0
- nanasqlite/cache.py +335 -0
- nanasqlite/core.py +2336 -0
- nanasqlite/exceptions.py +117 -0
- nanasqlite/py.typed +0 -0
- nanasqlite/sql_utils.py +174 -0
- nanasqlite/utils.py +202 -0
- nanasqlite-1.3.3.dev4.dist-info/METADATA +413 -0
- nanasqlite-1.3.3.dev4.dist-info/RECORD +13 -0
- nanasqlite-1.3.3.dev4.dist-info/WHEEL +5 -0
- nanasqlite-1.3.3.dev4.dist-info/licenses/LICENSE +21 -0
- nanasqlite-1.3.3.dev4.dist-info/top_level.txt +1 -0
nanasqlite/async_core.py
ADDED
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NanaSQLite Async Wrapper: Non-blocking async interface for NanaSQLite.
|
|
3
|
+
(NanaSQLite 非同期ラッパー: NanaSQLiteのための非ブロッキング非同期インターフェース)
|
|
4
|
+
|
|
5
|
+
Provides async/await support for all NanaSQLite operations, preventing blocking
|
|
6
|
+
in async applications by running database operations in a thread pool.
|
|
7
|
+
|
|
8
|
+
データベース操作をスレッドプールで実行することにより、非同期アプリケーションでのブロッキングを防ぎ、
|
|
9
|
+
すべてのNanaSQLite操作に対してasync/awaitサポートを提供します。
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> import asyncio
|
|
13
|
+
>>> from nanasqlite import AsyncNanaSQLite
|
|
14
|
+
>>>
|
|
15
|
+
>>> async def main():
|
|
16
|
+
... async with AsyncNanaSQLite("mydata.db") as db:
|
|
17
|
+
... await db.aset("user", {"name": "Nana", "age": 20})
|
|
18
|
+
... user = await db.aget("user")
|
|
19
|
+
... print(user)
|
|
20
|
+
>>>
|
|
21
|
+
>>> asyncio.run(main())
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
import queue
|
|
29
|
+
import re
|
|
30
|
+
import weakref
|
|
31
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
32
|
+
from contextlib import contextmanager
|
|
33
|
+
from typing import Any, Literal
|
|
34
|
+
|
|
35
|
+
import apsw
|
|
36
|
+
|
|
37
|
+
from .cache import CacheType
|
|
38
|
+
from .core import IDENTIFIER_PATTERN, NanaSQLite
|
|
39
|
+
from .exceptions import NanaSQLiteClosedError, NanaSQLiteDatabaseError
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AsyncNanaSQLite:
|
|
43
|
+
"""
|
|
44
|
+
Async wrapper for NanaSQLite with optimized thread pool executor.
|
|
45
|
+
(最適化されたスレッドプールを使用するNanaSQLiteの非同期ラッパー)
|
|
46
|
+
|
|
47
|
+
All database operations are executed in a dedicated thread pool executor to prevent
|
|
48
|
+
blocking the async event loop. This allows NanaSQLite to be used safely
|
|
49
|
+
in async applications like FastAPI, aiohttp, etc.
|
|
50
|
+
|
|
51
|
+
データベース操作はすべて専用のスレッドプール内で実行され、非同期イベントループのブロックを防ぎます。
|
|
52
|
+
これにより、FastAPIやaiohttpなどの非同期アプリケーションで安全に使用できます。
|
|
53
|
+
|
|
54
|
+
The implementation uses a configurable thread pool for optimal concurrency
|
|
55
|
+
and performance in high-load scenarios.
|
|
56
|
+
|
|
57
|
+
高負荷なシナリオにおいて最適な並行性とパフォーマンスを実現するため、
|
|
58
|
+
カスタマイズ可能なスレッドプールを使用しています。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
db_path: SQLiteデータベースファイルのパス
|
|
62
|
+
table: 使用するテーブル名 (デフォルト: "data")
|
|
63
|
+
bulk_load: Trueの場合、初期化時に全データをメモリに読み込む
|
|
64
|
+
optimize: Trueの場合、WALモードなど高速化設定を適用
|
|
65
|
+
cache_size_mb: SQLiteキャッシュサイズ(MB)、デフォルト64MB
|
|
66
|
+
strict_sql_validation: Trueの場合、未許可の関数等を含むクエリを拒否 (v1.2.0)
|
|
67
|
+
max_clause_length: SQL句の最大長(ReDoS対策、v1.2.0)
|
|
68
|
+
max_workers: スレッドプール内の最大ワーカー数(デフォルト: 5)
|
|
69
|
+
thread_name_prefix: スレッド名のプレフィックス(デフォルト: "AsyncNanaSQLite")
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> async with AsyncNanaSQLite("mydata.db") as db:
|
|
73
|
+
... await db.aset("config", {"theme": "dark"})
|
|
74
|
+
... config = await db.aget("config")
|
|
75
|
+
... print(config)
|
|
76
|
+
|
|
77
|
+
>>> # 高負荷環境向けの設定
|
|
78
|
+
>>> async with AsyncNanaSQLite("mydata.db", max_workers=10) as db:
|
|
79
|
+
... # 並行処理が多い場合に最適化
|
|
80
|
+
... results = await asyncio.gather(*[db.aget(f"key_{i}") for i in range(100)])
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
db_path: str,
|
|
86
|
+
table: str = "data",
|
|
87
|
+
bulk_load: bool = False,
|
|
88
|
+
optimize: bool = True,
|
|
89
|
+
cache_size_mb: int = 64,
|
|
90
|
+
max_workers: int = 5,
|
|
91
|
+
thread_name_prefix: str = "AsyncNanaSQLite",
|
|
92
|
+
strict_sql_validation: bool = True,
|
|
93
|
+
allowed_sql_functions: list[str] | None = None,
|
|
94
|
+
forbidden_sql_functions: list[str] | None = None,
|
|
95
|
+
max_clause_length: int | None = 1000,
|
|
96
|
+
read_pool_size: int = 0,
|
|
97
|
+
cache_strategy: CacheType | str = CacheType.UNBOUNDED,
|
|
98
|
+
cache_size: int | None = None,
|
|
99
|
+
cache_ttl: float | None = None,
|
|
100
|
+
cache_persistence_ttl: bool = False,
|
|
101
|
+
encryption_key: str | bytes | None = None,
|
|
102
|
+
encryption_mode: Literal["aes-gcm", "chacha20", "fernet"] = "aes-gcm",
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Args:
|
|
106
|
+
db_path: SQLiteデータベースファイルのパス
|
|
107
|
+
table: 使用するテーブル名 (デフォルト: "data")
|
|
108
|
+
bulk_load: Trueの場合、初期化時に全データをメモリに読み込む
|
|
109
|
+
optimize: Trueの場合、WALモードなど高速化設定を適用
|
|
110
|
+
cache_size_mb: SQLiteキャッシュサイズ(MB)、デフォルト64MB
|
|
111
|
+
max_workers: スレッドプール内の最大ワーカー数(デフォルト: 5)
|
|
112
|
+
thread_name_prefix: スレッド名のプレフィックス(デフォルト: "AsyncNanaSQLite")
|
|
113
|
+
strict_sql_validation: Trueの場合、未許可の関数等を含むクエリを拒否 (v1.2.0)
|
|
114
|
+
allowed_sql_functions: 追加で許可するSQL関数のリスト (v1.2.0)
|
|
115
|
+
forbidden_sql_functions: 明示的に禁止するSQL関数のリスト (v1.2.0)
|
|
116
|
+
max_clause_length: SQL句の最大長(ReDoS対策)。Noneで制限なし (v1.2.0)
|
|
117
|
+
read_pool_size: 読み取り専用プールサイズ (デフォルト: 0 = 無効) (v1.1.0)
|
|
118
|
+
encryption_key: 暗号化キー (v1.3.1)
|
|
119
|
+
"""
|
|
120
|
+
self._db_path = db_path
|
|
121
|
+
self._table = table
|
|
122
|
+
self._bulk_load = bulk_load
|
|
123
|
+
self._optimize = optimize
|
|
124
|
+
self._cache_size_mb = cache_size_mb
|
|
125
|
+
self._max_workers = max_workers
|
|
126
|
+
self._thread_name_prefix = thread_name_prefix
|
|
127
|
+
self._read_pool_size = read_pool_size
|
|
128
|
+
self._read_pool: queue.Queue | None = None
|
|
129
|
+
self._strict_sql_validation = strict_sql_validation
|
|
130
|
+
self._allowed_sql_functions = allowed_sql_functions
|
|
131
|
+
self._forbidden_sql_functions = forbidden_sql_functions
|
|
132
|
+
self._max_clause_length = max_clause_length
|
|
133
|
+
self._cache_strategy = cache_strategy
|
|
134
|
+
self._cache_size = cache_size
|
|
135
|
+
self._cache_ttl = cache_ttl
|
|
136
|
+
self._cache_persistence_ttl = cache_persistence_ttl
|
|
137
|
+
self._encryption_key = encryption_key
|
|
138
|
+
self._encryption_mode = encryption_mode
|
|
139
|
+
self._closed = False
|
|
140
|
+
self._child_instances = weakref.WeakSet() # WeakSetによる弱参照追跡(死んだ参照は自動的にクリーンアップ)
|
|
141
|
+
self._is_connection_owner = True
|
|
142
|
+
|
|
143
|
+
# 専用スレッドプールエグゼキューターを作成
|
|
144
|
+
self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix=thread_name_prefix)
|
|
145
|
+
self._db: NanaSQLite | None = None
|
|
146
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
147
|
+
self._owns_executor = True # このインスタンスがエグゼキューターを所有
|
|
148
|
+
|
|
149
|
+
async def _ensure_initialized(self) -> None:
|
|
150
|
+
"""Ensure the underlying sync database is initialized"""
|
|
151
|
+
if self._closed:
|
|
152
|
+
if not getattr(self, "_is_connection_owner", True):
|
|
153
|
+
raise NanaSQLiteClosedError(f"Parent database connection is closed (table: {self._table!r})")
|
|
154
|
+
raise NanaSQLiteClosedError("Database connection is closed")
|
|
155
|
+
|
|
156
|
+
if self._db is None:
|
|
157
|
+
# Initialize in thread pool to avoid blocking
|
|
158
|
+
loop = asyncio.get_running_loop()
|
|
159
|
+
self._loop = loop
|
|
160
|
+
self._db = await loop.run_in_executor(
|
|
161
|
+
self._executor,
|
|
162
|
+
lambda: NanaSQLite(
|
|
163
|
+
self._db_path,
|
|
164
|
+
table=self._table,
|
|
165
|
+
bulk_load=self._bulk_load,
|
|
166
|
+
optimize=self._optimize,
|
|
167
|
+
cache_size_mb=self._cache_size_mb,
|
|
168
|
+
strict_sql_validation=self._strict_sql_validation,
|
|
169
|
+
allowed_sql_functions=self._allowed_sql_functions,
|
|
170
|
+
forbidden_sql_functions=self._forbidden_sql_functions,
|
|
171
|
+
max_clause_length=self._max_clause_length,
|
|
172
|
+
cache_strategy=self._cache_strategy,
|
|
173
|
+
cache_size=self._cache_size,
|
|
174
|
+
cache_ttl=self._cache_ttl,
|
|
175
|
+
cache_persistence_ttl=self._cache_persistence_ttl,
|
|
176
|
+
encryption_key=self._encryption_key,
|
|
177
|
+
encryption_mode=self._encryption_mode,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Initialize Read-Only Pool if requested
|
|
182
|
+
if self._read_pool_size > 0:
|
|
183
|
+
self._read_pool = queue.Queue(maxsize=self._read_pool_size)
|
|
184
|
+
|
|
185
|
+
def _init_pool_connection():
|
|
186
|
+
# mode=ro (Read-Only) is mandatory for safety
|
|
187
|
+
flags = apsw.SQLITE_OPEN_READONLY | apsw.SQLITE_OPEN_URI
|
|
188
|
+
uri_path = f"file:{self._db_path}?mode=ro"
|
|
189
|
+
|
|
190
|
+
for _ in range(self._read_pool_size):
|
|
191
|
+
conn = apsw.Connection(uri_path, flags=flags)
|
|
192
|
+
# Apply optimizations to pool connections too (WAL, mmap)
|
|
193
|
+
# We use a cursor to set PRAGMAs
|
|
194
|
+
c = conn.cursor()
|
|
195
|
+
c.execute("PRAGMA journal_mode = WAL")
|
|
196
|
+
c.execute("PRAGMA synchronous = NORMAL")
|
|
197
|
+
c.execute("PRAGMA mmap_size = 268435456")
|
|
198
|
+
# Smaller cache for pool connections (don't hog memory)
|
|
199
|
+
c.execute("PRAGMA cache_size = -2000") # ~2MB
|
|
200
|
+
c.execute("PRAGMA temp_store = MEMORY")
|
|
201
|
+
self._read_pool.put(conn)
|
|
202
|
+
|
|
203
|
+
await loop.run_in_executor(self._executor, _init_pool_connection)
|
|
204
|
+
|
|
205
|
+
@contextmanager
|
|
206
|
+
def _read_connection(self):
|
|
207
|
+
"""
|
|
208
|
+
Context manager to yield a connection for read-only operations.
|
|
209
|
+
Yields a pooled connection if available, otherwise yields the main DB connection.
|
|
210
|
+
"""
|
|
211
|
+
if self._read_pool is None:
|
|
212
|
+
with self._db._lock:
|
|
213
|
+
yield self._db._connection
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Capture reference locally to ensure safety even if self._read_pool is set to None elsewhere
|
|
217
|
+
pool = self._read_pool
|
|
218
|
+
conn = pool.get()
|
|
219
|
+
try:
|
|
220
|
+
yield conn
|
|
221
|
+
finally:
|
|
222
|
+
pool.put(conn)
|
|
223
|
+
|
|
224
|
+
async def _run_in_executor(self, func, *args):
|
|
225
|
+
"""Run a synchronous function in the executor"""
|
|
226
|
+
await self._ensure_initialized()
|
|
227
|
+
loop = asyncio.get_running_loop()
|
|
228
|
+
return await loop.run_in_executor(self._executor, func, *args)
|
|
229
|
+
|
|
230
|
+
# ==================== Async Dict-like Interface ====================
|
|
231
|
+
|
|
232
|
+
async def aget(self, key: str, default: Any = None) -> Any:
|
|
233
|
+
"""
|
|
234
|
+
非同期でキーの値を取得
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
key: 取得するキー
|
|
238
|
+
default: キーが存在しない場合のデフォルト値
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
キーの値(存在しない場合はdefault)
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> user = await db.aget("user")
|
|
245
|
+
>>> config = await db.aget("config", {})
|
|
246
|
+
"""
|
|
247
|
+
await self._ensure_initialized()
|
|
248
|
+
loop = asyncio.get_running_loop()
|
|
249
|
+
return await loop.run_in_executor(self._executor, self._db.get, key, default)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def aset(self, key: str, value: Any) -> None:
|
|
253
|
+
"""
|
|
254
|
+
非同期でキーに値を設定
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
key: 設定するキー
|
|
258
|
+
value: 設定する値
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
>>> await db.aset("user", {"name": "Nana", "age": 20})
|
|
262
|
+
"""
|
|
263
|
+
await self._ensure_initialized()
|
|
264
|
+
loop = asyncio.get_running_loop()
|
|
265
|
+
await loop.run_in_executor(self._executor, self._db.__setitem__, key, value)
|
|
266
|
+
|
|
267
|
+
async def adelete(self, key: str) -> None:
|
|
268
|
+
"""
|
|
269
|
+
非同期でキーを削除
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
key: 削除するキー
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
KeyError: キーが存在しない場合
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> await db.adelete("old_data")
|
|
279
|
+
"""
|
|
280
|
+
await self._ensure_initialized()
|
|
281
|
+
loop = asyncio.get_running_loop()
|
|
282
|
+
await loop.run_in_executor(self._executor, self._db.__delitem__, key)
|
|
283
|
+
|
|
284
|
+
async def acontains(self, key: str) -> bool:
|
|
285
|
+
"""
|
|
286
|
+
非同期でキーの存在確認
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
key: 確認するキー
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
キーが存在する場合True
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
>>> if await db.acontains("user"):
|
|
296
|
+
... print("User exists")
|
|
297
|
+
"""
|
|
298
|
+
await self._ensure_initialized()
|
|
299
|
+
if self._db is None:
|
|
300
|
+
await self._ensure_initialized()
|
|
301
|
+
if self._db is None:
|
|
302
|
+
raise RuntimeError("Database not initialized")
|
|
303
|
+
loop = asyncio.get_running_loop()
|
|
304
|
+
return await loop.run_in_executor(self._executor, self._db.__contains__, key)
|
|
305
|
+
|
|
306
|
+
async def alen(self) -> int:
|
|
307
|
+
"""
|
|
308
|
+
非同期でデータベースの件数を取得
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
データベース内のキーの数
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
>>> count = await db.alen()
|
|
315
|
+
"""
|
|
316
|
+
await self._ensure_initialized()
|
|
317
|
+
loop = asyncio.get_running_loop()
|
|
318
|
+
return await loop.run_in_executor(self._executor, self._db.__len__)
|
|
319
|
+
|
|
320
|
+
async def akeys(self) -> list[str]:
|
|
321
|
+
"""
|
|
322
|
+
非同期で全キーを取得
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
全キーのリスト
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> keys = await db.akeys()
|
|
329
|
+
"""
|
|
330
|
+
await self._ensure_initialized()
|
|
331
|
+
loop = asyncio.get_running_loop()
|
|
332
|
+
return await loop.run_in_executor(self._executor, self._db.keys)
|
|
333
|
+
|
|
334
|
+
async def avalues(self) -> list[Any]:
|
|
335
|
+
"""
|
|
336
|
+
非同期で全値を取得
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
全値のリスト
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
>>> values = await db.avalues()
|
|
343
|
+
"""
|
|
344
|
+
await self._ensure_initialized()
|
|
345
|
+
loop = asyncio.get_running_loop()
|
|
346
|
+
return await loop.run_in_executor(self._executor, self._db.values)
|
|
347
|
+
|
|
348
|
+
async def aitems(self) -> list[tuple[str, Any]]:
|
|
349
|
+
"""
|
|
350
|
+
非同期で全アイテムを取得
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
全アイテムのリスト(キーと値のタプル)
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
>>> items = await db.aitems()
|
|
357
|
+
"""
|
|
358
|
+
await self._ensure_initialized()
|
|
359
|
+
loop = asyncio.get_running_loop()
|
|
360
|
+
return await loop.run_in_executor(self._executor, self._db.items)
|
|
361
|
+
|
|
362
|
+
async def apop(self, key: str, *args) -> Any:
|
|
363
|
+
"""
|
|
364
|
+
非同期でキーを削除して値を返す
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
key: 削除するキー
|
|
368
|
+
*args: デフォルト値(オプション)
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
削除されたキーの値
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
>>> value = await db.apop("temp_data")
|
|
375
|
+
>>> value = await db.apop("maybe_missing", "default")
|
|
376
|
+
"""
|
|
377
|
+
await self._ensure_initialized()
|
|
378
|
+
loop = asyncio.get_running_loop()
|
|
379
|
+
return await loop.run_in_executor(self._executor, self._db.pop, key, *args)
|
|
380
|
+
|
|
381
|
+
async def aupdate(self, mapping: dict = None, **kwargs) -> None:
|
|
382
|
+
"""
|
|
383
|
+
非同期で複数のキーを更新
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
mapping: 更新するキーと値のdict
|
|
387
|
+
**kwargs: キーワード引数として渡す更新
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
>>> await db.aupdate({"key1": "value1", "key2": "value2"})
|
|
391
|
+
>>> await db.aupdate(key3="value3", key4="value4")
|
|
392
|
+
"""
|
|
393
|
+
await self._ensure_initialized()
|
|
394
|
+
loop = asyncio.get_running_loop()
|
|
395
|
+
|
|
396
|
+
# Create a wrapper function that captures kwargs
|
|
397
|
+
def update_wrapper():
|
|
398
|
+
self._db.update(mapping, **kwargs)
|
|
399
|
+
|
|
400
|
+
await loop.run_in_executor(self._executor, update_wrapper)
|
|
401
|
+
|
|
402
|
+
async def aclear(self) -> None:
|
|
403
|
+
"""
|
|
404
|
+
非同期で全データを削除
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
>>> await db.aclear()
|
|
408
|
+
"""
|
|
409
|
+
await self._ensure_initialized()
|
|
410
|
+
loop = asyncio.get_running_loop()
|
|
411
|
+
await loop.run_in_executor(self._executor, self._db.clear)
|
|
412
|
+
|
|
413
|
+
async def asetdefault(self, key: str, default: Any = None) -> Any:
|
|
414
|
+
"""
|
|
415
|
+
非同期でキーが存在しない場合のみ値を設定
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
key: キー
|
|
419
|
+
default: デフォルト値
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
キーの値(既存または新規設定した値)
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
>>> value = await db.asetdefault("config", {})
|
|
426
|
+
"""
|
|
427
|
+
await self._ensure_initialized()
|
|
428
|
+
loop = asyncio.get_running_loop()
|
|
429
|
+
return await loop.run_in_executor(self._executor, self._db.setdefault, key, default)
|
|
430
|
+
|
|
431
|
+
# ==================== Async Special Methods ====================
|
|
432
|
+
|
|
433
|
+
async def load_all(self) -> None:
|
|
434
|
+
"""
|
|
435
|
+
非同期で全データを一括ロード
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> await db.load_all()
|
|
439
|
+
"""
|
|
440
|
+
await self._ensure_initialized()
|
|
441
|
+
loop = asyncio.get_running_loop()
|
|
442
|
+
await loop.run_in_executor(self._executor, self._db.load_all)
|
|
443
|
+
|
|
444
|
+
async def refresh(self, key: str = None) -> None:
|
|
445
|
+
"""
|
|
446
|
+
非同期でキャッシュを更新
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
key: 更新するキー(Noneの場合は全キャッシュ)
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> await db.refresh("user")
|
|
453
|
+
>>> await db.refresh() # 全キャッシュ更新
|
|
454
|
+
"""
|
|
455
|
+
await self._ensure_initialized()
|
|
456
|
+
loop = asyncio.get_running_loop()
|
|
457
|
+
await loop.run_in_executor(self._executor, self._db.refresh, key)
|
|
458
|
+
|
|
459
|
+
async def is_cached(self, key: str) -> bool:
|
|
460
|
+
"""
|
|
461
|
+
非同期でキーがキャッシュ済みか確認
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
key: 確認するキー
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
キャッシュ済みの場合True
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
>>> cached = await db.is_cached("user")
|
|
471
|
+
"""
|
|
472
|
+
await self._ensure_initialized()
|
|
473
|
+
loop = asyncio.get_running_loop()
|
|
474
|
+
return await loop.run_in_executor(self._executor, self._db.is_cached, key)
|
|
475
|
+
|
|
476
|
+
async def batch_update(self, mapping: dict[str, Any]) -> None:
|
|
477
|
+
"""
|
|
478
|
+
非同期で一括書き込み(高速)
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
mapping: 書き込むキーと値のdict
|
|
482
|
+
|
|
483
|
+
Example:
|
|
484
|
+
>>> await db.batch_update({
|
|
485
|
+
... "key1": "value1",
|
|
486
|
+
... "key2": "value2",
|
|
487
|
+
... "key3": {"nested": "data"}
|
|
488
|
+
... })
|
|
489
|
+
"""
|
|
490
|
+
await self._ensure_initialized()
|
|
491
|
+
loop = asyncio.get_running_loop()
|
|
492
|
+
await loop.run_in_executor(self._executor, self._db.batch_update, mapping)
|
|
493
|
+
|
|
494
|
+
async def batch_delete(self, keys: list[str]) -> None:
|
|
495
|
+
"""
|
|
496
|
+
非同期で一括削除(高速)
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
keys: 削除するキーのリスト
|
|
500
|
+
|
|
501
|
+
Example:
|
|
502
|
+
>>> await db.batch_delete(["key1", "key2", "key3"])
|
|
503
|
+
"""
|
|
504
|
+
await self._ensure_initialized()
|
|
505
|
+
loop = asyncio.get_running_loop()
|
|
506
|
+
await loop.run_in_executor(self._executor, self._db.batch_delete, keys)
|
|
507
|
+
|
|
508
|
+
async def to_dict(self) -> dict:
|
|
509
|
+
"""
|
|
510
|
+
非同期で全データをPython dictとして取得
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
全データを含むdict
|
|
514
|
+
|
|
515
|
+
Example:
|
|
516
|
+
>>> data = await db.to_dict()
|
|
517
|
+
"""
|
|
518
|
+
await self._ensure_initialized()
|
|
519
|
+
loop = asyncio.get_running_loop()
|
|
520
|
+
return await loop.run_in_executor(self._executor, self._db.to_dict)
|
|
521
|
+
|
|
522
|
+
async def copy(self) -> dict:
|
|
523
|
+
"""
|
|
524
|
+
非同期で浅いコピーを作成
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
全データのコピー
|
|
528
|
+
|
|
529
|
+
Example:
|
|
530
|
+
>>> data_copy = await db.copy()
|
|
531
|
+
"""
|
|
532
|
+
await self._ensure_initialized()
|
|
533
|
+
loop = asyncio.get_running_loop()
|
|
534
|
+
return await loop.run_in_executor(self._executor, self._db.copy)
|
|
535
|
+
|
|
536
|
+
async def get_fresh(self, key: str, default: Any = None) -> Any:
|
|
537
|
+
"""
|
|
538
|
+
非同期でDBから直接読み込み、キャッシュを更新
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
key: 取得するキー
|
|
542
|
+
default: キーが存在しない場合のデフォルト値
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
DBから取得した最新の値
|
|
546
|
+
|
|
547
|
+
Example:
|
|
548
|
+
>>> value = await db.get_fresh("key")
|
|
549
|
+
"""
|
|
550
|
+
await self._ensure_initialized()
|
|
551
|
+
loop = asyncio.get_running_loop()
|
|
552
|
+
return await loop.run_in_executor(self._executor, self._db.get_fresh, key, default)
|
|
553
|
+
|
|
554
|
+
async def abatch_get(self, keys: list[str]) -> dict[str, Any]:
|
|
555
|
+
"""
|
|
556
|
+
非同期で複数のキーを一度に取得
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
keys: 取得するキーのリスト
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
取得に成功したキーと値の dict
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
>>> results = await db.abatch_get(["key1", "key2"])
|
|
566
|
+
"""
|
|
567
|
+
await self._ensure_initialized()
|
|
568
|
+
loop = asyncio.get_running_loop()
|
|
569
|
+
return await loop.run_in_executor(self._executor, self._db.batch_get, keys)
|
|
570
|
+
|
|
571
|
+
# ==================== Async Pydantic Support ====================
|
|
572
|
+
|
|
573
|
+
async def set_model(self, key: str, model: Any) -> None:
|
|
574
|
+
"""
|
|
575
|
+
非同期でPydanticモデルを保存
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
key: 保存するキー
|
|
579
|
+
model: Pydanticモデルのインスタンス
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
>>> from pydantic import BaseModel
|
|
583
|
+
>>> class User(BaseModel):
|
|
584
|
+
... name: str
|
|
585
|
+
... age: int
|
|
586
|
+
>>> user = User(name="Nana", age=20)
|
|
587
|
+
>>> await db.set_model("user", user)
|
|
588
|
+
"""
|
|
589
|
+
await self._ensure_initialized()
|
|
590
|
+
loop = asyncio.get_running_loop()
|
|
591
|
+
await loop.run_in_executor(self._executor, self._db.set_model, key, model)
|
|
592
|
+
|
|
593
|
+
async def get_model(self, key: str, model_class: type = None) -> Any:
|
|
594
|
+
"""
|
|
595
|
+
非同期でPydanticモデルを取得
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
key: 取得するキー
|
|
599
|
+
model_class: Pydanticモデルのクラス
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Pydanticモデルのインスタンス
|
|
603
|
+
|
|
604
|
+
Example:
|
|
605
|
+
>>> user = await db.get_model("user", User)
|
|
606
|
+
"""
|
|
607
|
+
await self._ensure_initialized()
|
|
608
|
+
loop = asyncio.get_running_loop()
|
|
609
|
+
return await loop.run_in_executor(self._executor, self._db.get_model, key, model_class)
|
|
610
|
+
|
|
611
|
+
# ==================== Async SQL Execution ====================
|
|
612
|
+
|
|
613
|
+
async def execute(self, sql: str, parameters: tuple | None = None) -> Any:
|
|
614
|
+
"""
|
|
615
|
+
非同期でSQLを直接実行
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
sql: 実行するSQL文
|
|
619
|
+
parameters: SQLのパラメータ
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
APSWのCursorオブジェクト
|
|
623
|
+
|
|
624
|
+
Example:
|
|
625
|
+
>>> cursor = await db.execute("SELECT * FROM data WHERE key LIKE ?", ("user%",))
|
|
626
|
+
"""
|
|
627
|
+
await self._ensure_initialized()
|
|
628
|
+
loop = asyncio.get_running_loop()
|
|
629
|
+
return await loop.run_in_executor(self._executor, self._db.execute, sql, parameters)
|
|
630
|
+
|
|
631
|
+
async def execute_many(self, sql: str, parameters_list: list[tuple]) -> None:
|
|
632
|
+
"""
|
|
633
|
+
非同期でSQLを複数のパラメータで一括実行
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
sql: 実行するSQL文
|
|
637
|
+
parameters_list: パラメータのリスト
|
|
638
|
+
|
|
639
|
+
Example:
|
|
640
|
+
>>> await db.execute_many(
|
|
641
|
+
... "INSERT OR REPLACE INTO custom (id, name) VALUES (?, ?)",
|
|
642
|
+
... [(1, "Alice"), (2, "Bob"), (3, "Charlie")]
|
|
643
|
+
... )
|
|
644
|
+
"""
|
|
645
|
+
await self._ensure_initialized()
|
|
646
|
+
loop = asyncio.get_running_loop()
|
|
647
|
+
await loop.run_in_executor(self._executor, self._db.execute_many, sql, parameters_list)
|
|
648
|
+
|
|
649
|
+
async def fetch_one(self, sql: str, parameters: tuple = None) -> tuple | None:
|
|
650
|
+
"""
|
|
651
|
+
非同期でSQLを実行して1行取得
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
sql: 実行するSQL文
|
|
655
|
+
parameters: SQLのパラメータ
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
1行の結果(tuple)
|
|
659
|
+
|
|
660
|
+
Example:
|
|
661
|
+
>>> row = await db.fetch_one("SELECT value FROM data WHERE key = ?", ("user",))
|
|
662
|
+
"""
|
|
663
|
+
await self._ensure_initialized()
|
|
664
|
+
|
|
665
|
+
def _fetch_one_impl():
|
|
666
|
+
with self._read_connection() as conn:
|
|
667
|
+
cursor = conn.execute(sql, parameters)
|
|
668
|
+
return cursor.fetchone()
|
|
669
|
+
|
|
670
|
+
loop = asyncio.get_running_loop()
|
|
671
|
+
return await loop.run_in_executor(self._executor, _fetch_one_impl)
|
|
672
|
+
|
|
673
|
+
async def fetch_all(self, sql: str, parameters: tuple = None) -> list[tuple]:
|
|
674
|
+
"""
|
|
675
|
+
非同期でSQLを実行して全行取得
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
sql: 実行するSQL文
|
|
679
|
+
parameters: SQLのパラメータ
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
全行の結果(tupleのリスト)
|
|
683
|
+
|
|
684
|
+
Example:
|
|
685
|
+
>>> rows = await db.fetch_all("SELECT key, value FROM data WHERE key LIKE ?", ("user%",))
|
|
686
|
+
"""
|
|
687
|
+
await self._ensure_initialized()
|
|
688
|
+
|
|
689
|
+
def _fetch_all_impl():
|
|
690
|
+
with self._read_connection() as conn:
|
|
691
|
+
cursor = conn.execute(sql, parameters)
|
|
692
|
+
return list(cursor)
|
|
693
|
+
|
|
694
|
+
loop = asyncio.get_running_loop()
|
|
695
|
+
return await loop.run_in_executor(self._executor, _fetch_all_impl)
|
|
696
|
+
|
|
697
|
+
# ==================== Async SQLite Wrapper Functions ====================
|
|
698
|
+
|
|
699
|
+
async def create_table(
|
|
700
|
+
self, table_name: str, columns: dict, if_not_exists: bool = True, primary_key: str = None
|
|
701
|
+
) -> None:
|
|
702
|
+
"""
|
|
703
|
+
非同期でテーブルを作成
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
table_name: テーブル名
|
|
707
|
+
columns: カラム定義のdict
|
|
708
|
+
if_not_exists: Trueの場合、存在しない場合のみ作成
|
|
709
|
+
primary_key: プライマリキーのカラム名
|
|
710
|
+
|
|
711
|
+
Example:
|
|
712
|
+
>>> await db.create_table("users", {
|
|
713
|
+
... "id": "INTEGER PRIMARY KEY",
|
|
714
|
+
... "name": "TEXT NOT NULL",
|
|
715
|
+
... "email": "TEXT UNIQUE"
|
|
716
|
+
... })
|
|
717
|
+
"""
|
|
718
|
+
await self._ensure_initialized()
|
|
719
|
+
loop = asyncio.get_running_loop()
|
|
720
|
+
await loop.run_in_executor(
|
|
721
|
+
self._executor, self._db.create_table, table_name, columns, if_not_exists, primary_key
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
async def create_index(
|
|
725
|
+
self, index_name: str, table_name: str, columns: list[str], unique: bool = False, if_not_exists: bool = True
|
|
726
|
+
) -> None:
|
|
727
|
+
"""
|
|
728
|
+
非同期でインデックスを作成
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
index_name: インデックス名
|
|
732
|
+
table_name: テーブル名
|
|
733
|
+
columns: インデックスを作成するカラムのリスト
|
|
734
|
+
unique: Trueの場合、ユニークインデックスを作成
|
|
735
|
+
if_not_exists: Trueの場合、存在しない場合のみ作成
|
|
736
|
+
|
|
737
|
+
Example:
|
|
738
|
+
>>> await db.create_index("idx_users_email", "users", ["email"], unique=True)
|
|
739
|
+
"""
|
|
740
|
+
await self._ensure_initialized()
|
|
741
|
+
loop = asyncio.get_running_loop()
|
|
742
|
+
await loop.run_in_executor(
|
|
743
|
+
self._executor, self._db.create_index, index_name, table_name, columns, unique, if_not_exists
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
async def query(
|
|
747
|
+
self,
|
|
748
|
+
table_name: str = None,
|
|
749
|
+
columns: list[str] = None,
|
|
750
|
+
where: str = None,
|
|
751
|
+
parameters: tuple = None,
|
|
752
|
+
order_by: str = None,
|
|
753
|
+
limit: int = None,
|
|
754
|
+
strict_sql_validation: bool = None,
|
|
755
|
+
allowed_sql_functions: list[str] = None,
|
|
756
|
+
forbidden_sql_functions: list[str] = None,
|
|
757
|
+
override_allowed: bool = False,
|
|
758
|
+
) -> list[dict]:
|
|
759
|
+
"""
|
|
760
|
+
非同期でSELECTクエリを実行
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
table_name: テーブル名
|
|
764
|
+
columns: 取得するカラムのリスト
|
|
765
|
+
where: WHERE句の条件
|
|
766
|
+
parameters: WHERE句のパラメータ
|
|
767
|
+
order_by: ORDER BY句
|
|
768
|
+
limit: LIMIT句
|
|
769
|
+
strict_sql_validation: Trueの場合、未許可の関数等を含むクエリを拒否
|
|
770
|
+
allowed_sql_functions: このクエリで一時的に許可するSQL関数のリスト
|
|
771
|
+
forbidden_sql_functions: このクエリで一時的に禁止するSQL関数のリスト
|
|
772
|
+
override_allowed: Trueの場合、インスタンス許可設定を無視
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
結果のリスト(各行はdict)
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
>>> results = await db.query(
|
|
779
|
+
... table_name="users",
|
|
780
|
+
... columns=["id", "name", "email"],
|
|
781
|
+
... where="age > ?",
|
|
782
|
+
... parameters=(20,),
|
|
783
|
+
... order_by="name ASC",
|
|
784
|
+
... limit=10
|
|
785
|
+
... )
|
|
786
|
+
"""
|
|
787
|
+
await self._ensure_initialized()
|
|
788
|
+
|
|
789
|
+
loop = asyncio.get_running_loop()
|
|
790
|
+
return await loop.run_in_executor(
|
|
791
|
+
self._executor,
|
|
792
|
+
self._shared_query_impl,
|
|
793
|
+
table_name,
|
|
794
|
+
columns,
|
|
795
|
+
where,
|
|
796
|
+
parameters,
|
|
797
|
+
order_by,
|
|
798
|
+
limit,
|
|
799
|
+
None, # offset
|
|
800
|
+
None, # group_by
|
|
801
|
+
strict_sql_validation,
|
|
802
|
+
allowed_sql_functions,
|
|
803
|
+
forbidden_sql_functions,
|
|
804
|
+
override_allowed,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
async def query_with_pagination(
|
|
808
|
+
self,
|
|
809
|
+
table_name: str = None,
|
|
810
|
+
columns: list[str] = None,
|
|
811
|
+
where: str = None,
|
|
812
|
+
parameters: tuple = None,
|
|
813
|
+
order_by: str = None,
|
|
814
|
+
limit: int = None,
|
|
815
|
+
offset: int = None,
|
|
816
|
+
group_by: str = None,
|
|
817
|
+
strict_sql_validation: bool = None,
|
|
818
|
+
allowed_sql_functions: list[str] = None,
|
|
819
|
+
forbidden_sql_functions: list[str] = None,
|
|
820
|
+
override_allowed: bool = False,
|
|
821
|
+
) -> list[dict]:
|
|
822
|
+
"""
|
|
823
|
+
非同期で拡張されたクエリを実行
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
table_name: テーブル名
|
|
827
|
+
columns: 取得するカラム
|
|
828
|
+
where: WHERE句
|
|
829
|
+
parameters: パラメータ
|
|
830
|
+
order_by: ORDER BY句
|
|
831
|
+
limit: LIMIT句
|
|
832
|
+
offset: OFFSET句
|
|
833
|
+
group_by: GROUP BY句
|
|
834
|
+
strict_sql_validation: Trueの場合、未許可の関数等を含むクエリを拒否
|
|
835
|
+
allowed_sql_functions: このクエリで一時的に許可するSQL関数のリスト
|
|
836
|
+
forbidden_sql_functions: このクエリで一時的に禁止するSQL関数のリスト
|
|
837
|
+
override_allowed: Trueの場合、インスタンス許可設定を無視
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
結果のリスト(各行はdict)
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> results = await db.query_with_pagination(
|
|
844
|
+
... table_name="users",
|
|
845
|
+
... columns=["id", "name", "email"],
|
|
846
|
+
... where="age > ?",
|
|
847
|
+
... parameters=(20,),
|
|
848
|
+
... order_by="name ASC",
|
|
849
|
+
... limit=10,
|
|
850
|
+
... offset=0
|
|
851
|
+
... )
|
|
852
|
+
"""
|
|
853
|
+
if self._db is None:
|
|
854
|
+
await self._ensure_initialized()
|
|
855
|
+
|
|
856
|
+
loop = asyncio.get_running_loop()
|
|
857
|
+
return await loop.run_in_executor(
|
|
858
|
+
self._executor,
|
|
859
|
+
self._shared_query_impl,
|
|
860
|
+
table_name,
|
|
861
|
+
columns,
|
|
862
|
+
where,
|
|
863
|
+
parameters,
|
|
864
|
+
order_by,
|
|
865
|
+
limit,
|
|
866
|
+
offset,
|
|
867
|
+
group_by,
|
|
868
|
+
strict_sql_validation,
|
|
869
|
+
allowed_sql_functions,
|
|
870
|
+
forbidden_sql_functions,
|
|
871
|
+
override_allowed,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
async def table_exists(self, table_name: str) -> bool:
|
|
875
|
+
"""
|
|
876
|
+
非同期でテーブルの存在確認
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
table_name: テーブル名
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
存在する場合True
|
|
883
|
+
|
|
884
|
+
Example:
|
|
885
|
+
>>> exists = await db.table_exists("users")
|
|
886
|
+
"""
|
|
887
|
+
await self._ensure_initialized()
|
|
888
|
+
loop = asyncio.get_running_loop()
|
|
889
|
+
return await loop.run_in_executor(self._executor, self._db.table_exists, table_name)
|
|
890
|
+
|
|
891
|
+
async def list_tables(self) -> list[str]:
|
|
892
|
+
"""
|
|
893
|
+
非同期でデータベース内の全テーブル一覧を取得
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
テーブル名のリスト
|
|
897
|
+
|
|
898
|
+
Example:
|
|
899
|
+
>>> tables = await db.list_tables()
|
|
900
|
+
"""
|
|
901
|
+
await self._ensure_initialized()
|
|
902
|
+
loop = asyncio.get_running_loop()
|
|
903
|
+
return await loop.run_in_executor(self._executor, self._db.list_tables)
|
|
904
|
+
|
|
905
|
+
async def drop_table(self, table_name: str, if_exists: bool = True) -> None:
|
|
906
|
+
"""
|
|
907
|
+
非同期でテーブルを削除
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
table_name: テーブル名
|
|
911
|
+
if_exists: Trueの場合、存在する場合のみ削除
|
|
912
|
+
|
|
913
|
+
Example:
|
|
914
|
+
>>> await db.drop_table("old_table")
|
|
915
|
+
"""
|
|
916
|
+
await self._ensure_initialized()
|
|
917
|
+
loop = asyncio.get_running_loop()
|
|
918
|
+
await loop.run_in_executor(self._executor, self._db.drop_table, table_name, if_exists)
|
|
919
|
+
|
|
920
|
+
async def drop_index(self, index_name: str, if_exists: bool = True) -> None:
|
|
921
|
+
"""
|
|
922
|
+
非同期でインデックスを削除
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
index_name: インデックス名
|
|
926
|
+
if_exists: Trueの場合、存在する場合のみ削除
|
|
927
|
+
|
|
928
|
+
Example:
|
|
929
|
+
>>> await db.drop_index("idx_users_email")
|
|
930
|
+
"""
|
|
931
|
+
await self._ensure_initialized()
|
|
932
|
+
loop = asyncio.get_running_loop()
|
|
933
|
+
await loop.run_in_executor(self._executor, self._db.drop_index, index_name, if_exists)
|
|
934
|
+
|
|
935
|
+
async def sql_insert(self, table_name: str, data: dict) -> int:
|
|
936
|
+
"""
|
|
937
|
+
非同期でdictから直接INSERT
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
table_name: テーブル名
|
|
941
|
+
data: カラム名と値のdict
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
挿入されたROWID
|
|
945
|
+
|
|
946
|
+
Example:
|
|
947
|
+
>>> rowid = await db.sql_insert("users", {
|
|
948
|
+
... "name": "Alice",
|
|
949
|
+
... "email": "alice@example.com",
|
|
950
|
+
... "age": 25
|
|
951
|
+
... })
|
|
952
|
+
"""
|
|
953
|
+
await self._ensure_initialized()
|
|
954
|
+
loop = asyncio.get_running_loop()
|
|
955
|
+
return await loop.run_in_executor(self._executor, self._db.sql_insert, table_name, data)
|
|
956
|
+
|
|
957
|
+
async def sql_update(self, table_name: str, data: dict, where: str, parameters: tuple = None) -> int:
|
|
958
|
+
"""
|
|
959
|
+
非同期でdictとwhere条件でUPDATE
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
table_name: テーブル名
|
|
963
|
+
data: 更新するカラム名と値のdict
|
|
964
|
+
where: WHERE句の条件
|
|
965
|
+
parameters: WHERE句のパラメータ
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
更新された行数
|
|
969
|
+
|
|
970
|
+
Example:
|
|
971
|
+
>>> count = await db.sql_update("users",
|
|
972
|
+
... {"age": 26, "status": "active"},
|
|
973
|
+
... "name = ?",
|
|
974
|
+
... ("Alice",)
|
|
975
|
+
... )
|
|
976
|
+
"""
|
|
977
|
+
await self._ensure_initialized()
|
|
978
|
+
loop = asyncio.get_running_loop()
|
|
979
|
+
return await loop.run_in_executor(self._executor, self._db.sql_update, table_name, data, where, parameters)
|
|
980
|
+
|
|
981
|
+
async def sql_delete(self, table_name: str, where: str, parameters: tuple = None) -> int:
|
|
982
|
+
"""
|
|
983
|
+
非同期でwhere条件でDELETE
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
table_name: テーブル名
|
|
987
|
+
where: WHERE句の条件
|
|
988
|
+
parameters: WHERE句のパラメータ
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
削除された行数
|
|
992
|
+
|
|
993
|
+
Example:
|
|
994
|
+
>>> count = await db.sql_delete("users", "age < ?", (18,))
|
|
995
|
+
"""
|
|
996
|
+
await self._ensure_initialized()
|
|
997
|
+
loop = asyncio.get_running_loop()
|
|
998
|
+
return await loop.run_in_executor(self._executor, self._db.sql_delete, table_name, where, parameters)
|
|
999
|
+
|
|
1000
|
+
async def count(
|
|
1001
|
+
self,
|
|
1002
|
+
table_name: str = None,
|
|
1003
|
+
where: str = None,
|
|
1004
|
+
parameters: tuple = None,
|
|
1005
|
+
strict_sql_validation: bool = None,
|
|
1006
|
+
allowed_sql_functions: list[str] = None,
|
|
1007
|
+
forbidden_sql_functions: list[str] = None,
|
|
1008
|
+
override_allowed: bool = False,
|
|
1009
|
+
) -> int:
|
|
1010
|
+
"""
|
|
1011
|
+
非同期でレコード数を取得
|
|
1012
|
+
|
|
1013
|
+
Args:
|
|
1014
|
+
table_name: テーブル名
|
|
1015
|
+
where: WHERE句の条件
|
|
1016
|
+
parameters: WHERE句のパラメータ
|
|
1017
|
+
strict_sql_validation: Trueの場合、未許可の関数等を含むクエリを拒否
|
|
1018
|
+
allowed_sql_functions: このクエリで一時的に許可するSQL関数のリスト
|
|
1019
|
+
forbidden_sql_functions: このクエリで一時的に禁止するSQL関数のリスト
|
|
1020
|
+
override_allowed: Trueの場合、インスタンス許可設定を無視
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
レコード数
|
|
1024
|
+
|
|
1025
|
+
Example:
|
|
1026
|
+
>>> count = await db.count("users", "age < ?", (18,))
|
|
1027
|
+
"""
|
|
1028
|
+
await self._ensure_initialized()
|
|
1029
|
+
loop = asyncio.get_running_loop()
|
|
1030
|
+
return await loop.run_in_executor(
|
|
1031
|
+
self._executor,
|
|
1032
|
+
self._db.count,
|
|
1033
|
+
table_name,
|
|
1034
|
+
where,
|
|
1035
|
+
parameters,
|
|
1036
|
+
strict_sql_validation,
|
|
1037
|
+
allowed_sql_functions,
|
|
1038
|
+
forbidden_sql_functions,
|
|
1039
|
+
override_allowed,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
async def vacuum(self) -> None:
|
|
1043
|
+
"""
|
|
1044
|
+
非同期でデータベースを最適化(VACUUM実行)
|
|
1045
|
+
|
|
1046
|
+
Example:
|
|
1047
|
+
>>> await db.vacuum()
|
|
1048
|
+
"""
|
|
1049
|
+
await self._ensure_initialized()
|
|
1050
|
+
loop = asyncio.get_running_loop()
|
|
1051
|
+
await loop.run_in_executor(self._executor, self._db.vacuum)
|
|
1052
|
+
|
|
1053
|
+
# ==================== Transaction Control ====================
|
|
1054
|
+
|
|
1055
|
+
async def begin_transaction(self) -> None:
|
|
1056
|
+
"""
|
|
1057
|
+
非同期でトランザクションを開始
|
|
1058
|
+
|
|
1059
|
+
Example:
|
|
1060
|
+
>>> await db.begin_transaction()
|
|
1061
|
+
>>> try:
|
|
1062
|
+
... await db.sql_insert("users", {"name": "Alice"})
|
|
1063
|
+
... await db.sql_insert("users", {"name": "Bob"})
|
|
1064
|
+
... await db.commit()
|
|
1065
|
+
... except:
|
|
1066
|
+
... await db.rollback()
|
|
1067
|
+
"""
|
|
1068
|
+
await self._ensure_initialized()
|
|
1069
|
+
loop = asyncio.get_running_loop()
|
|
1070
|
+
await loop.run_in_executor(self._executor, self._db.begin_transaction)
|
|
1071
|
+
|
|
1072
|
+
async def commit(self) -> None:
|
|
1073
|
+
"""
|
|
1074
|
+
非同期でトランザクションをコミット
|
|
1075
|
+
|
|
1076
|
+
Example:
|
|
1077
|
+
>>> await db.commit()
|
|
1078
|
+
"""
|
|
1079
|
+
await self._ensure_initialized()
|
|
1080
|
+
loop = asyncio.get_running_loop()
|
|
1081
|
+
await loop.run_in_executor(self._executor, self._db.commit)
|
|
1082
|
+
|
|
1083
|
+
async def rollback(self) -> None:
|
|
1084
|
+
"""
|
|
1085
|
+
非同期でトランザクションをロールバック
|
|
1086
|
+
|
|
1087
|
+
Example:
|
|
1088
|
+
>>> await db.rollback()
|
|
1089
|
+
"""
|
|
1090
|
+
await self._ensure_initialized()
|
|
1091
|
+
loop = asyncio.get_running_loop()
|
|
1092
|
+
await loop.run_in_executor(self._executor, self._db.rollback)
|
|
1093
|
+
|
|
1094
|
+
async def in_transaction(self) -> bool:
|
|
1095
|
+
"""
|
|
1096
|
+
非同期でトランザクション状態を確認
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
bool: トランザクション中の場合True
|
|
1100
|
+
|
|
1101
|
+
Example:
|
|
1102
|
+
>>> status = await db.in_transaction()
|
|
1103
|
+
>>> print(f"In transaction: {status}")
|
|
1104
|
+
"""
|
|
1105
|
+
await self._ensure_initialized()
|
|
1106
|
+
loop = asyncio.get_running_loop()
|
|
1107
|
+
return await loop.run_in_executor(self._executor, self._db.in_transaction)
|
|
1108
|
+
|
|
1109
|
+
def transaction(self):
|
|
1110
|
+
"""
|
|
1111
|
+
非同期トランザクションのコンテキストマネージャ
|
|
1112
|
+
|
|
1113
|
+
Example:
|
|
1114
|
+
>>> async with db.transaction():
|
|
1115
|
+
... await db.sql_insert("users", {"name": "Alice"})
|
|
1116
|
+
... await db.sql_insert("users", {"name": "Bob"})
|
|
1117
|
+
... # 自動的にコミット、例外時はロールバック
|
|
1118
|
+
"""
|
|
1119
|
+
return _AsyncTransactionContext(self)
|
|
1120
|
+
|
|
1121
|
+
# ==================== Context Manager Support ====================
|
|
1122
|
+
|
|
1123
|
+
async def __aenter__(self):
|
|
1124
|
+
"""Async context manager entry"""
|
|
1125
|
+
await self._ensure_initialized()
|
|
1126
|
+
return self
|
|
1127
|
+
|
|
1128
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
1129
|
+
"""Async context manager exit"""
|
|
1130
|
+
await self.close()
|
|
1131
|
+
return False
|
|
1132
|
+
|
|
1133
|
+
async def aclear_cache(self) -> None:
|
|
1134
|
+
"""
|
|
1135
|
+
メモリキャッシュをクリア (非同期)
|
|
1136
|
+
|
|
1137
|
+
DBのデータは削除せず、メモリ上のキャッシュのみ破棄します。
|
|
1138
|
+
"""
|
|
1139
|
+
if self._db is None:
|
|
1140
|
+
return
|
|
1141
|
+
loop = asyncio.get_running_loop()
|
|
1142
|
+
await loop.run_in_executor(self._executor, self._db.clear_cache)
|
|
1143
|
+
|
|
1144
|
+
async def clear_cache(self) -> None:
|
|
1145
|
+
"""aclear_cache のエイリアス"""
|
|
1146
|
+
await self.aclear_cache()
|
|
1147
|
+
|
|
1148
|
+
async def close(self) -> None:
|
|
1149
|
+
"""
|
|
1150
|
+
非同期でデータベース接続を閉じる
|
|
1151
|
+
|
|
1152
|
+
スレッドプールエグゼキューターもシャットダウンします。
|
|
1153
|
+
|
|
1154
|
+
Example:
|
|
1155
|
+
>>> await db.close()
|
|
1156
|
+
"""
|
|
1157
|
+
if self._closed:
|
|
1158
|
+
return
|
|
1159
|
+
|
|
1160
|
+
if self._db is not None:
|
|
1161
|
+
loop = asyncio.get_running_loop()
|
|
1162
|
+
await loop.run_in_executor(self._executor, self._db.close)
|
|
1163
|
+
self._db = None
|
|
1164
|
+
|
|
1165
|
+
self._closed = True
|
|
1166
|
+
|
|
1167
|
+
# 子インスタンスに通知
|
|
1168
|
+
for child in self._child_instances:
|
|
1169
|
+
child._mark_parent_closed()
|
|
1170
|
+
self._child_instances.clear()
|
|
1171
|
+
|
|
1172
|
+
# Close Read-Only Pool
|
|
1173
|
+
if self._read_pool:
|
|
1174
|
+
while True:
|
|
1175
|
+
conn = None
|
|
1176
|
+
try:
|
|
1177
|
+
conn = self._read_pool.get_nowait()
|
|
1178
|
+
conn.close()
|
|
1179
|
+
except queue.Empty:
|
|
1180
|
+
# Queue is empty; safe to stop draining
|
|
1181
|
+
break
|
|
1182
|
+
except AttributeError as e:
|
|
1183
|
+
# Programming error: conn is not an apsw.Connection
|
|
1184
|
+
logging.getLogger(__name__).error(
|
|
1185
|
+
"AttributeError during pool cleanup - possible programming error: %s (conn=%r)",
|
|
1186
|
+
e,
|
|
1187
|
+
conn,
|
|
1188
|
+
)
|
|
1189
|
+
# continue draining the queue instead of breaking
|
|
1190
|
+
except apsw.Error as e:
|
|
1191
|
+
# Ignore close errors during best-effort cleanup but log at warning level
|
|
1192
|
+
logging.getLogger(__name__).warning(
|
|
1193
|
+
"Error while closing read-only NanaSQLite connection %r: %s",
|
|
1194
|
+
conn,
|
|
1195
|
+
e,
|
|
1196
|
+
)
|
|
1197
|
+
self._read_pool = None
|
|
1198
|
+
|
|
1199
|
+
# 所有しているエグゼキューターをシャットダウン(ノンブロッキング)
|
|
1200
|
+
if self._owns_executor and self._executor is not None:
|
|
1201
|
+
loop = asyncio.get_running_loop()
|
|
1202
|
+
await loop.run_in_executor(None, self._executor.shutdown, True)
|
|
1203
|
+
self._executor = None
|
|
1204
|
+
|
|
1205
|
+
def _mark_parent_closed(self) -> None:
|
|
1206
|
+
"""親インスタンスが閉じられた際に呼ばれる"""
|
|
1207
|
+
self._closed = True
|
|
1208
|
+
self._db = None
|
|
1209
|
+
# 子がさらに子を持っている場合も再帰的に閉じる
|
|
1210
|
+
for child in self._child_instances:
|
|
1211
|
+
child._mark_parent_closed()
|
|
1212
|
+
self._child_instances.clear()
|
|
1213
|
+
|
|
1214
|
+
def __repr__(self) -> str:
|
|
1215
|
+
if self._db is not None:
|
|
1216
|
+
return f"AsyncNanaSQLite({self._db_path!r}, table={self._table!r}, max_workers={self._max_workers}, initialized=True)"
|
|
1217
|
+
return f"AsyncNanaSQLite({self._db_path!r}, table={self._table!r}, max_workers={self._max_workers}, initialized=False)"
|
|
1218
|
+
|
|
1219
|
+
# ==================== Sync DB Access (for advanced use) ====================
|
|
1220
|
+
|
|
1221
|
+
@property
|
|
1222
|
+
def sync_db(self) -> NanaSQLite | None:
|
|
1223
|
+
"""
|
|
1224
|
+
同期DBインスタンスへのアクセス(上級者向け)
|
|
1225
|
+
|
|
1226
|
+
Warning:
|
|
1227
|
+
このプロパティは上級者向けです。
|
|
1228
|
+
非同期コンテキストで同期操作を行うとイベントループがブロックされる可能性があります。
|
|
1229
|
+
通常は非同期メソッドを使用してください。
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
内部のNanaSQLiteインスタンス
|
|
1233
|
+
"""
|
|
1234
|
+
return self._db
|
|
1235
|
+
|
|
1236
|
+
async def table(self, table_name: str) -> AsyncNanaSQLite:
|
|
1237
|
+
"""
|
|
1238
|
+
非同期でサブテーブルのAsyncNanaSQLiteインスタンスを取得
|
|
1239
|
+
|
|
1240
|
+
既に初期化済みの親インスタンスから呼ばれることを想定しています。
|
|
1241
|
+
接続とエグゼキューターは親インスタンスと共有されます。
|
|
1242
|
+
|
|
1243
|
+
⚠️ 重要な注意事項:
|
|
1244
|
+
- 同じテーブルに対して複数のインスタンスを作成しないでください
|
|
1245
|
+
各インスタンスは独立したキャッシュを持つため、キャッシュ不整合が発生します
|
|
1246
|
+
- 推奨: テーブルインスタンスを変数に保存して再利用してください
|
|
1247
|
+
|
|
1248
|
+
非推奨:
|
|
1249
|
+
sub1 = await db.table("users")
|
|
1250
|
+
sub2 = await db.table("users") # キャッシュ不整合の原因
|
|
1251
|
+
|
|
1252
|
+
推奨:
|
|
1253
|
+
users_db = await db.table("users")
|
|
1254
|
+
# users_dbを使い回す
|
|
1255
|
+
|
|
1256
|
+
Args:
|
|
1257
|
+
table_name: 取得するサブテーブル名
|
|
1258
|
+
|
|
1259
|
+
Returns:
|
|
1260
|
+
指定したテーブルを操作するAsyncNanaSQLiteインスタンス
|
|
1261
|
+
|
|
1262
|
+
Example:
|
|
1263
|
+
>>> async with AsyncNanaSQLite("mydata.db", table="main") as db:
|
|
1264
|
+
... users_db = await db.table("users")
|
|
1265
|
+
... products_db = await db.table("products")
|
|
1266
|
+
... await users_db.aset("user1", {"name": "Alice"})
|
|
1267
|
+
... await products_db.aset("prod1", {"name": "Laptop"})
|
|
1268
|
+
"""
|
|
1269
|
+
# 親インスタンスが初期化済みであることを確認
|
|
1270
|
+
if self._db is None:
|
|
1271
|
+
await self._ensure_initialized()
|
|
1272
|
+
|
|
1273
|
+
loop = asyncio.get_running_loop()
|
|
1274
|
+
sub_db = await loop.run_in_executor(self._executor, self._db.table, table_name)
|
|
1275
|
+
|
|
1276
|
+
# 新しいAsyncNanaSQLiteラッパーを作成(__init__をバイパス)
|
|
1277
|
+
async_sub_db = object.__new__(AsyncNanaSQLite)
|
|
1278
|
+
async_sub_db._db_path = self._db_path
|
|
1279
|
+
async_sub_db._table = table_name
|
|
1280
|
+
async_sub_db._bulk_load = self._bulk_load
|
|
1281
|
+
async_sub_db._optimize = self._optimize
|
|
1282
|
+
async_sub_db._cache_size_mb = self._cache_size_mb
|
|
1283
|
+
async_sub_db._max_workers = self._max_workers
|
|
1284
|
+
async_sub_db._thread_name_prefix = self._thread_name_prefix + f"_{table_name}"
|
|
1285
|
+
async_sub_db._db = sub_db # 接続を共有した同期版DBを設定
|
|
1286
|
+
async_sub_db._closed = False # クローズ状態を初期化
|
|
1287
|
+
async_sub_db._loop = loop # イベントループを共有
|
|
1288
|
+
async_sub_db._executor = self._executor # 同じエグゼキューターを共有
|
|
1289
|
+
async_sub_db._owns_executor = False # エグゼキューターは所有しない
|
|
1290
|
+
async_sub_db._is_connection_owner = False # 接続の所有権はない
|
|
1291
|
+
# セキュリティ関連の設定も親インスタンスから継承する
|
|
1292
|
+
async_sub_db._strict_sql_validation = self._strict_sql_validation
|
|
1293
|
+
async_sub_db._allowed_sql_functions = self._allowed_sql_functions
|
|
1294
|
+
async_sub_db._forbidden_sql_functions = self._forbidden_sql_functions
|
|
1295
|
+
async_sub_db._max_clause_length = self._max_clause_length
|
|
1296
|
+
# 子インスタンス管理
|
|
1297
|
+
async_sub_db._child_instances = weakref.WeakSet()
|
|
1298
|
+
self._child_instances.add(async_sub_db)
|
|
1299
|
+
|
|
1300
|
+
# Read-Only Pool は sub-instance では使用しない (シンプルさと後方互換性のため)
|
|
1301
|
+
async_sub_db._read_pool_size = 0
|
|
1302
|
+
async_sub_db._read_pool = None
|
|
1303
|
+
return async_sub_db
|
|
1304
|
+
|
|
1305
|
+
# ==================== Async Method Aliases (Consistency & Stability) ====================
|
|
1306
|
+
# For a fully 'a'-prefixed API and compatibility with all tests/benchmarks
|
|
1307
|
+
|
|
1308
|
+
aload_all = load_all
|
|
1309
|
+
arefresh = refresh
|
|
1310
|
+
ais_cached = is_cached
|
|
1311
|
+
abatch_update = batch_update
|
|
1312
|
+
abatch_delete = batch_delete
|
|
1313
|
+
ato_dict = to_dict
|
|
1314
|
+
acopy = copy
|
|
1315
|
+
aget_fresh = get_fresh
|
|
1316
|
+
aset_model = set_model
|
|
1317
|
+
aget_model = get_model
|
|
1318
|
+
aexecute = execute
|
|
1319
|
+
aexecute_many = execute_many
|
|
1320
|
+
afetch_one = fetch_one
|
|
1321
|
+
afetch_all = fetch_all
|
|
1322
|
+
acreate_table = create_table
|
|
1323
|
+
acreate_index = create_index
|
|
1324
|
+
aquery = query
|
|
1325
|
+
aquery_with_pagination = query_with_pagination
|
|
1326
|
+
atable = table
|
|
1327
|
+
atable_exists = table_exists
|
|
1328
|
+
alist_tables = list_tables
|
|
1329
|
+
adrop_table = drop_table
|
|
1330
|
+
asql_insert = sql_insert
|
|
1331
|
+
asql_update = sql_update
|
|
1332
|
+
asql_delete = sql_delete
|
|
1333
|
+
acount = count
|
|
1334
|
+
avacuum = vacuum
|
|
1335
|
+
|
|
1336
|
+
def _shared_query_impl(
|
|
1337
|
+
self,
|
|
1338
|
+
table_name: str,
|
|
1339
|
+
columns: list[str],
|
|
1340
|
+
where: str,
|
|
1341
|
+
parameters: tuple,
|
|
1342
|
+
order_by: str,
|
|
1343
|
+
limit: int,
|
|
1344
|
+
offset: int = None,
|
|
1345
|
+
group_by: str = None,
|
|
1346
|
+
strict_sql_validation: bool = None,
|
|
1347
|
+
allowed_sql_functions: list[str] = None,
|
|
1348
|
+
forbidden_sql_functions: list[str] = None,
|
|
1349
|
+
override_allowed: bool = False,
|
|
1350
|
+
) -> list[dict]:
|
|
1351
|
+
"""Internal shared implementation for query execution"""
|
|
1352
|
+
target_table = self._db._sanitize_identifier(table_name) if table_name else self._db._table
|
|
1353
|
+
|
|
1354
|
+
# Validation (Delegated to Main Instance logic)
|
|
1355
|
+
v_args = {
|
|
1356
|
+
"strict": strict_sql_validation,
|
|
1357
|
+
"allowed": allowed_sql_functions,
|
|
1358
|
+
"forbidden": forbidden_sql_functions,
|
|
1359
|
+
"override_allowed": override_allowed,
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
# table_name is already validated via _sanitize_identifier above
|
|
1363
|
+
self._db._validate_expression(where, **v_args, context="where")
|
|
1364
|
+
self._db._validate_expression(order_by, **v_args, context="order_by")
|
|
1365
|
+
self._db._validate_expression(group_by, **v_args, context="group_by")
|
|
1366
|
+
if columns:
|
|
1367
|
+
for col in columns:
|
|
1368
|
+
self._db._validate_expression(col, **v_args, context="column")
|
|
1369
|
+
|
|
1370
|
+
# Column handling
|
|
1371
|
+
if columns is None:
|
|
1372
|
+
columns_sql = "*"
|
|
1373
|
+
else:
|
|
1374
|
+
safe_cols = []
|
|
1375
|
+
for col in columns:
|
|
1376
|
+
if IDENTIFIER_PATTERN.match(col):
|
|
1377
|
+
safe_cols.append(self._db._sanitize_identifier(col))
|
|
1378
|
+
else:
|
|
1379
|
+
safe_cols.append(col)
|
|
1380
|
+
columns_sql = ", ".join(safe_cols)
|
|
1381
|
+
|
|
1382
|
+
# Validate limit
|
|
1383
|
+
if limit is not None:
|
|
1384
|
+
if not isinstance(limit, int):
|
|
1385
|
+
raise ValueError(f"limit must be an integer, got {type(limit).__name__}")
|
|
1386
|
+
if limit < 0:
|
|
1387
|
+
raise ValueError("limit must be non-negative")
|
|
1388
|
+
|
|
1389
|
+
# SQL Construction
|
|
1390
|
+
sql = f"SELECT {columns_sql} FROM {target_table}" # nosec
|
|
1391
|
+
if where:
|
|
1392
|
+
sql += f" WHERE {where}"
|
|
1393
|
+
if group_by:
|
|
1394
|
+
sql += f" GROUP BY {group_by}"
|
|
1395
|
+
if order_by:
|
|
1396
|
+
sql += f" ORDER BY {order_by}"
|
|
1397
|
+
if limit is not None:
|
|
1398
|
+
sql += f" LIMIT {limit}"
|
|
1399
|
+
if offset is not None:
|
|
1400
|
+
sql += f" OFFSET {offset}"
|
|
1401
|
+
|
|
1402
|
+
# Execute on pool
|
|
1403
|
+
try:
|
|
1404
|
+
with self._read_connection() as conn:
|
|
1405
|
+
cursor = conn.cursor()
|
|
1406
|
+
cursor.execute(sql, parameters)
|
|
1407
|
+
|
|
1408
|
+
# Column name extraction using cursor metadata (robust against AS alias parsing issues)
|
|
1409
|
+
try:
|
|
1410
|
+
description = cursor.getdescription()
|
|
1411
|
+
col_names = [col_info[0] for col_info in description]
|
|
1412
|
+
except apsw.ExecutionCompleteError:
|
|
1413
|
+
# Fallback for zero-row results (e.g., limit=0)
|
|
1414
|
+
if columns is None:
|
|
1415
|
+
# Get column names from table metadata
|
|
1416
|
+
p_cursor = conn.cursor()
|
|
1417
|
+
p_cursor.execute(f"PRAGMA table_info({target_table})")
|
|
1418
|
+
col_names = [row[1] for row in p_cursor]
|
|
1419
|
+
else:
|
|
1420
|
+
# Extract aliases from provided columns list
|
|
1421
|
+
col_names = []
|
|
1422
|
+
for col in columns:
|
|
1423
|
+
parts = re.split(r"\s+as\s+", col, flags=re.IGNORECASE)
|
|
1424
|
+
if len(parts) > 1:
|
|
1425
|
+
col_names.append(parts[-1].strip().strip('"').strip("'"))
|
|
1426
|
+
else:
|
|
1427
|
+
col_names.append(col.strip())
|
|
1428
|
+
|
|
1429
|
+
# Convert to dict list
|
|
1430
|
+
return [dict(zip(col_names, row)) for row in cursor]
|
|
1431
|
+
except apsw.Error as e:
|
|
1432
|
+
raise NanaSQLiteDatabaseError(f"Failed to execute query: {e}", original_error=e) from e
|
|
1433
|
+
|
|
1434
|
+
get = aget
|
|
1435
|
+
contains = acontains
|
|
1436
|
+
keys = akeys
|
|
1437
|
+
values = avalues
|
|
1438
|
+
items = aitems
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
class _AsyncTransactionContext:
|
|
1442
|
+
"""非同期トランザクションのコンテキストマネージャ"""
|
|
1443
|
+
|
|
1444
|
+
def __init__(self, db: AsyncNanaSQLite):
|
|
1445
|
+
self.db = db
|
|
1446
|
+
|
|
1447
|
+
async def __aenter__(self):
|
|
1448
|
+
await self.db.begin_transaction()
|
|
1449
|
+
return self.db
|
|
1450
|
+
|
|
1451
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
1452
|
+
if exc_type is None:
|
|
1453
|
+
await self.db.commit()
|
|
1454
|
+
else:
|
|
1455
|
+
await self.db.rollback()
|
|
1456
|
+
return False
|