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.
@@ -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