bazaar-contrib-sqlite 0.3.0rc1__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.
- bazaar_contrib_sqlite/__init__.py +26 -0
- bazaar_contrib_sqlite/compiler.py +74 -0
- bazaar_contrib_sqlite/database.py +323 -0
- bazaar_contrib_sqlite/executor.py +673 -0
- bazaar_contrib_sqlite/py.typed +0 -0
- bazaar_contrib_sqlite/resources/add_column.sql.tpl +3 -0
- bazaar_contrib_sqlite/resources/busy_timeout.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/copy.sql.tpl +3 -0
- bazaar_contrib_sqlite/resources/create_index.sql.tpl +13 -0
- bazaar_contrib_sqlite/resources/create_table.sql.tpl +21 -0
- bazaar_contrib_sqlite/resources/create_trigger.sql.tpl +7 -0
- bazaar_contrib_sqlite/resources/delete.sql.tpl +6 -0
- bazaar_contrib_sqlite/resources/drop_column.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_index.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_table.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_trigger.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/fragments.sql.tpl +111 -0
- bazaar_contrib_sqlite/resources/insert.sql.tpl +23 -0
- bazaar_contrib_sqlite/resources/rename_column.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/rename_table.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/search_delete.sql.tpl +8 -0
- bazaar_contrib_sqlite/resources/search_insert.sql.tpl +8 -0
- bazaar_contrib_sqlite/resources/search_rebuild.sql.tpl +2 -0
- bazaar_contrib_sqlite/resources/search_table.sql.tpl +7 -0
- bazaar_contrib_sqlite/resources/search_update.sql.tpl +12 -0
- bazaar_contrib_sqlite/resources/select.sql.tpl +44 -0
- bazaar_contrib_sqlite/resources/update.sql.tpl +10 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/METADATA +35 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/RECORD +32 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/WHEEL +4 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/entry_points.txt +3 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import sqlite3
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
|
|
8
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from time import monotonic
|
|
12
|
+
from types import TracebackType
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
|
|
15
|
+
import aiosqlite
|
|
16
|
+
from bazaar_core import TaskFailureSignal
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SqliteExecutorClosedError(RuntimeError):
|
|
24
|
+
"""The SQLite executor is not accepting new work."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SqliteQueryTimeoutError(TimeoutError):
|
|
28
|
+
"""SQLite interrupted a query after its connection's execution budget."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class SqliteExecuteResult:
|
|
33
|
+
rowcount: int
|
|
34
|
+
lastrowid: int | None
|
|
35
|
+
rows: tuple[aiosqlite.Row, ...]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SqliteSession:
|
|
39
|
+
"""SQL operations bound to one executor-owned connection."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, connection: aiosqlite.Connection) -> None:
|
|
42
|
+
self._connection = connection
|
|
43
|
+
|
|
44
|
+
@asynccontextmanager
|
|
45
|
+
async def query_budget(self, seconds: float) -> AsyncIterator[None]:
|
|
46
|
+
"""Bound SQL execution and remove the handler before reusing the session."""
|
|
47
|
+
|
|
48
|
+
deadline = monotonic() + seconds
|
|
49
|
+
expired = False
|
|
50
|
+
|
|
51
|
+
def progress() -> int:
|
|
52
|
+
nonlocal expired
|
|
53
|
+
expired = monotonic() >= deadline
|
|
54
|
+
return int(expired)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
await self._connection.set_progress_handler(progress, 1_000)
|
|
58
|
+
yield
|
|
59
|
+
except sqlite3.OperationalError as error:
|
|
60
|
+
if expired and error.sqlite_errorcode == sqlite3.SQLITE_INTERRUPT:
|
|
61
|
+
raise SqliteQueryTimeoutError(
|
|
62
|
+
"SQLite query exceeded its execution budget"
|
|
63
|
+
) from error
|
|
64
|
+
raise
|
|
65
|
+
except asyncio.CancelledError:
|
|
66
|
+
await self._connection.interrupt()
|
|
67
|
+
raise
|
|
68
|
+
finally:
|
|
69
|
+
# sqlite3 removes the handler with None; aiosqlite's annotation omits it.
|
|
70
|
+
await asyncio.shield(
|
|
71
|
+
self._connection.set_progress_handler(None, 0) # type: ignore[arg-type]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def _call[T](self, operation: Awaitable[T]) -> T:
|
|
75
|
+
task = asyncio.ensure_future(operation)
|
|
76
|
+
try:
|
|
77
|
+
return await asyncio.shield(task)
|
|
78
|
+
except asyncio.CancelledError:
|
|
79
|
+
await self._connection.interrupt()
|
|
80
|
+
while not task.done():
|
|
81
|
+
try:
|
|
82
|
+
await asyncio.shield(task)
|
|
83
|
+
except asyncio.CancelledError:
|
|
84
|
+
pass
|
|
85
|
+
except Exception: # noqa: BLE001 - Preserve cancellation after interrupting SQLite.
|
|
86
|
+
break
|
|
87
|
+
if not task.cancelled():
|
|
88
|
+
task.exception()
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
async def execute(
|
|
92
|
+
self,
|
|
93
|
+
statement: str,
|
|
94
|
+
parameters: Sequence[object] = (),
|
|
95
|
+
) -> SqliteExecuteResult:
|
|
96
|
+
async def query() -> SqliteExecuteResult:
|
|
97
|
+
cursor = await self._connection.execute(statement, parameters)
|
|
98
|
+
try:
|
|
99
|
+
rows = tuple(await cursor.fetchall())
|
|
100
|
+
return SqliteExecuteResult(
|
|
101
|
+
rowcount=cursor.rowcount, lastrowid=cursor.lastrowid, rows=rows
|
|
102
|
+
)
|
|
103
|
+
finally:
|
|
104
|
+
await cursor.close()
|
|
105
|
+
|
|
106
|
+
return await self._call(query())
|
|
107
|
+
|
|
108
|
+
async def executemany(
|
|
109
|
+
self,
|
|
110
|
+
statement: str,
|
|
111
|
+
parameter_sets: Iterable[Sequence[object]],
|
|
112
|
+
) -> SqliteExecuteResult:
|
|
113
|
+
async def query() -> SqliteExecuteResult:
|
|
114
|
+
cursor = await self._connection.executemany(statement, parameter_sets)
|
|
115
|
+
try:
|
|
116
|
+
rows = tuple(await cursor.fetchall())
|
|
117
|
+
return SqliteExecuteResult(
|
|
118
|
+
rowcount=cursor.rowcount, lastrowid=cursor.lastrowid, rows=rows
|
|
119
|
+
)
|
|
120
|
+
finally:
|
|
121
|
+
await cursor.close()
|
|
122
|
+
|
|
123
|
+
return await self._call(query())
|
|
124
|
+
|
|
125
|
+
async def fetchone(
|
|
126
|
+
self,
|
|
127
|
+
statement: str,
|
|
128
|
+
parameters: Sequence[object] = (),
|
|
129
|
+
) -> aiosqlite.Row | None:
|
|
130
|
+
async def query() -> aiosqlite.Row | None:
|
|
131
|
+
cursor = await self._connection.execute(statement, parameters)
|
|
132
|
+
try:
|
|
133
|
+
return await cursor.fetchone()
|
|
134
|
+
finally:
|
|
135
|
+
await cursor.close()
|
|
136
|
+
|
|
137
|
+
return await self._call(query())
|
|
138
|
+
|
|
139
|
+
async def fetchall(
|
|
140
|
+
self,
|
|
141
|
+
statement: str,
|
|
142
|
+
parameters: Sequence[object] = (),
|
|
143
|
+
) -> list[aiosqlite.Row]:
|
|
144
|
+
async def query() -> list[aiosqlite.Row]:
|
|
145
|
+
cursor = await self._connection.execute(statement, parameters)
|
|
146
|
+
try:
|
|
147
|
+
return list(await cursor.fetchall())
|
|
148
|
+
finally:
|
|
149
|
+
await cursor.close()
|
|
150
|
+
|
|
151
|
+
return await self._call(query())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SqliteReadSession(SqliteSession):
|
|
155
|
+
def transaction(self) -> AbstractAsyncContextManager[SqliteReadSession]:
|
|
156
|
+
return _ReadTransaction(self)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _TransactionState(Enum):
|
|
160
|
+
NEW = auto()
|
|
161
|
+
ACTIVE = auto()
|
|
162
|
+
FINISHED = auto()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _ExecutorState(Enum):
|
|
166
|
+
CREATED = auto()
|
|
167
|
+
RUNNING = auto()
|
|
168
|
+
STOPPING = auto()
|
|
169
|
+
CLOSED = auto()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _ReadTransaction(AbstractAsyncContextManager[SqliteReadSession]):
|
|
173
|
+
def __init__(self, session: SqliteReadSession) -> None:
|
|
174
|
+
self._session = session
|
|
175
|
+
self._state = _TransactionState.NEW
|
|
176
|
+
|
|
177
|
+
async def __aenter__(self) -> SqliteReadSession:
|
|
178
|
+
await self._session._connection.execute("BEGIN")
|
|
179
|
+
self._state = _TransactionState.ACTIVE
|
|
180
|
+
return self._session
|
|
181
|
+
|
|
182
|
+
async def __aexit__(
|
|
183
|
+
self,
|
|
184
|
+
exc_type: type[BaseException] | None,
|
|
185
|
+
exc_value: BaseException | None,
|
|
186
|
+
traceback: TracebackType | None,
|
|
187
|
+
) -> bool:
|
|
188
|
+
del exc_value, traceback
|
|
189
|
+
if self._state is not _TransactionState.ACTIVE:
|
|
190
|
+
return False
|
|
191
|
+
statement = "ROLLBACK" if exc_type is not None else "COMMIT"
|
|
192
|
+
try:
|
|
193
|
+
if exc_type is not None and issubclass(exc_type, asyncio.CancelledError):
|
|
194
|
+
await self._session._connection.interrupt()
|
|
195
|
+
await asyncio.shield(self._session._connection.execute(statement))
|
|
196
|
+
except BaseException:
|
|
197
|
+
if exc_type is None:
|
|
198
|
+
await asyncio.shield(self._session._connection.execute("ROLLBACK"))
|
|
199
|
+
raise
|
|
200
|
+
finally:
|
|
201
|
+
self._state = _TransactionState.FINISHED
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class _WriteTransaction(AbstractAsyncContextManager[SqliteSession]):
|
|
206
|
+
def __init__(self, session: SqliteSession) -> None:
|
|
207
|
+
self._session = session
|
|
208
|
+
self._state = _TransactionState.NEW
|
|
209
|
+
|
|
210
|
+
async def __aenter__(self) -> SqliteSession:
|
|
211
|
+
try:
|
|
212
|
+
await self._session.execute("BEGIN IMMEDIATE")
|
|
213
|
+
except BaseException:
|
|
214
|
+
if self._session._connection.in_transaction:
|
|
215
|
+
await asyncio.shield(self._session._connection.execute("ROLLBACK"))
|
|
216
|
+
raise
|
|
217
|
+
self._state = _TransactionState.ACTIVE
|
|
218
|
+
return self._session
|
|
219
|
+
|
|
220
|
+
async def __aexit__(
|
|
221
|
+
self,
|
|
222
|
+
exc_type: type[BaseException] | None,
|
|
223
|
+
exc_value: BaseException | None,
|
|
224
|
+
traceback: TracebackType | None,
|
|
225
|
+
) -> bool:
|
|
226
|
+
del exc_value, traceback
|
|
227
|
+
if self._state is not _TransactionState.ACTIVE:
|
|
228
|
+
return False
|
|
229
|
+
statement = "ROLLBACK" if exc_type is not None else "COMMIT"
|
|
230
|
+
try:
|
|
231
|
+
await asyncio.shield(self._session._connection.execute(statement))
|
|
232
|
+
except BaseException:
|
|
233
|
+
if exc_type is None:
|
|
234
|
+
await asyncio.shield(self._session._connection.execute("ROLLBACK"))
|
|
235
|
+
raise
|
|
236
|
+
finally:
|
|
237
|
+
self._state = _TransactionState.FINISHED
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass(frozen=True, slots=True)
|
|
242
|
+
class _Queued:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(frozen=True, slots=True)
|
|
247
|
+
class _Executing[T]:
|
|
248
|
+
task: asyncio.Task[T]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass(frozen=True, slots=True)
|
|
252
|
+
class _Cancelled[T]:
|
|
253
|
+
task: asyncio.Task[T] | None = None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True, slots=True)
|
|
257
|
+
class _Finished:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(slots=True)
|
|
262
|
+
class _WriteRequest[T]:
|
|
263
|
+
operation: Callable[[SqliteSession], Awaitable[T]]
|
|
264
|
+
transactional: bool
|
|
265
|
+
result: asyncio.Future[T]
|
|
266
|
+
done: asyncio.Event = field(default_factory=asyncio.Event)
|
|
267
|
+
state: _Queued | _Executing[T] | _Cancelled[T] | _Finished = field(
|
|
268
|
+
default_factory=_Queued
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class _IdleReader:
|
|
274
|
+
connection: aiosqlite.Connection
|
|
275
|
+
idle_since: float
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class _ReaderLease(AbstractAsyncContextManager[SqliteReadSession]):
|
|
279
|
+
def __init__(self, executor: SqliteExecutor) -> None:
|
|
280
|
+
self._executor = executor
|
|
281
|
+
self._connection: aiosqlite.Connection | None = None
|
|
282
|
+
|
|
283
|
+
async def __aenter__(self) -> SqliteReadSession:
|
|
284
|
+
connection = await self._executor._borrow_reader()
|
|
285
|
+
self._connection = connection
|
|
286
|
+
return SqliteReadSession(connection)
|
|
287
|
+
|
|
288
|
+
async def __aexit__(
|
|
289
|
+
self,
|
|
290
|
+
exc_type: type[BaseException] | None,
|
|
291
|
+
exc_value: BaseException | None,
|
|
292
|
+
traceback: TracebackType | None,
|
|
293
|
+
) -> bool:
|
|
294
|
+
del exc_type, exc_value, traceback
|
|
295
|
+
connection = self._connection
|
|
296
|
+
self._connection = None
|
|
297
|
+
if connection is not None:
|
|
298
|
+
await self._executor._return_reader(connection)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class SqliteExecutor:
|
|
303
|
+
"""Single-writer actor and bounded read-session pool."""
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
writer_connection: aiosqlite.Connection,
|
|
308
|
+
read_connections: Sequence[aiosqlite.Connection],
|
|
309
|
+
*,
|
|
310
|
+
reader_factory: Callable[[], Awaitable[aiosqlite.Connection]],
|
|
311
|
+
max_idle_readers: int,
|
|
312
|
+
max_readers: int | None,
|
|
313
|
+
reader_idle_timeout: float,
|
|
314
|
+
tasks: asyncio.TaskGroup,
|
|
315
|
+
) -> None:
|
|
316
|
+
if not read_connections:
|
|
317
|
+
raise ValueError("at least one read connection is required")
|
|
318
|
+
self._tasks = tasks
|
|
319
|
+
self._writer_connection = writer_connection
|
|
320
|
+
self._reader_factory = reader_factory
|
|
321
|
+
self._max_idle_readers = max_idle_readers
|
|
322
|
+
self._max_readers = max_readers
|
|
323
|
+
self._reader_idle_timeout = reader_idle_timeout
|
|
324
|
+
self._read_connections = set(read_connections)
|
|
325
|
+
self._idle_readers: deque[_IdleReader] = deque()
|
|
326
|
+
for connection in read_connections:
|
|
327
|
+
self._idle_readers.append(_IdleReader(connection, monotonic()))
|
|
328
|
+
self._write_queue = asyncio.Queue[_WriteRequest[object] | None]()
|
|
329
|
+
self._writer_task: asyncio.Task[None] | None = None
|
|
330
|
+
self._reader_reaper_task: asyncio.Task[None] | None = None
|
|
331
|
+
self._failure = TaskFailureSignal()
|
|
332
|
+
self._reader_reaper_stop = asyncio.Event()
|
|
333
|
+
self._state = _ExecutorState.CREATED
|
|
334
|
+
self._borrowed_readers = 0
|
|
335
|
+
self._opening_readers = 0
|
|
336
|
+
self._reader_condition = asyncio.Condition()
|
|
337
|
+
self._current_request: _WriteRequest[object] | None = None
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def is_started(self) -> bool:
|
|
341
|
+
return self._writer_task is not None
|
|
342
|
+
|
|
343
|
+
async def start(self) -> None:
|
|
344
|
+
if self._writer_task is not None:
|
|
345
|
+
return
|
|
346
|
+
self._state = _ExecutorState.RUNNING
|
|
347
|
+
self._failure.reset()
|
|
348
|
+
self._reader_reaper_stop.clear()
|
|
349
|
+
self._writer_task = self._tasks.create_task(
|
|
350
|
+
self._failure.observe(self._run_writer(), component="SQLite writer"),
|
|
351
|
+
name="bcn-sqlite-writer",
|
|
352
|
+
)
|
|
353
|
+
self._reader_reaper_task = self._tasks.create_task(
|
|
354
|
+
self._failure.observe(
|
|
355
|
+
self._reap_idle_readers(), component="SQLite reader reaper"
|
|
356
|
+
),
|
|
357
|
+
name="bcn-sqlite-reader-reaper",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def stop(self) -> None:
|
|
361
|
+
if self._state is _ExecutorState.CLOSED:
|
|
362
|
+
return
|
|
363
|
+
writer_task = self._writer_task
|
|
364
|
+
if writer_task is not None and writer_task.done():
|
|
365
|
+
await self.abort()
|
|
366
|
+
return
|
|
367
|
+
self._failure.disable()
|
|
368
|
+
self._state = _ExecutorState.STOPPING
|
|
369
|
+
await self._write_queue.join()
|
|
370
|
+
async with self._reader_condition:
|
|
371
|
+
self._reader_condition.notify_all()
|
|
372
|
+
await self._reader_condition.wait_for(
|
|
373
|
+
lambda: self._borrowed_readers == 0 and self._opening_readers == 0
|
|
374
|
+
)
|
|
375
|
+
await self._stop_reader_reaper()
|
|
376
|
+
writer_task = self._writer_task
|
|
377
|
+
if writer_task is not None:
|
|
378
|
+
await self._write_queue.put(None)
|
|
379
|
+
await writer_task
|
|
380
|
+
self._writer_task = None
|
|
381
|
+
for connection in tuple(self._read_connections):
|
|
382
|
+
await connection.close()
|
|
383
|
+
self._read_connections.clear()
|
|
384
|
+
await self._writer_connection.close()
|
|
385
|
+
self._state = _ExecutorState.CLOSED
|
|
386
|
+
|
|
387
|
+
async def abort(self) -> None:
|
|
388
|
+
"""Interrupt active work and close all connections after a failed drain."""
|
|
389
|
+
if self._state is _ExecutorState.CLOSED:
|
|
390
|
+
return
|
|
391
|
+
self._failure.disable()
|
|
392
|
+
self._state = _ExecutorState.STOPPING
|
|
393
|
+
async with self._reader_condition:
|
|
394
|
+
self._reader_condition.notify_all()
|
|
395
|
+
request = self._current_request
|
|
396
|
+
if request is not None:
|
|
397
|
+
if not request.result.done():
|
|
398
|
+
request.result.set_exception(
|
|
399
|
+
SqliteExecutorClosedError(
|
|
400
|
+
"SQLite executor stopped before active work completed"
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
state = request.state
|
|
404
|
+
operation_task = (
|
|
405
|
+
state.task if isinstance(state, (_Executing, _Cancelled)) else None
|
|
406
|
+
)
|
|
407
|
+
if operation_task is not None and not operation_task.done():
|
|
408
|
+
operation_task.cancel()
|
|
409
|
+
await self._writer_connection.interrupt()
|
|
410
|
+
for connection in tuple(self._read_connections):
|
|
411
|
+
await connection.interrupt()
|
|
412
|
+
|
|
413
|
+
while True:
|
|
414
|
+
try:
|
|
415
|
+
queued = self._write_queue.get_nowait()
|
|
416
|
+
except asyncio.QueueEmpty:
|
|
417
|
+
break
|
|
418
|
+
try:
|
|
419
|
+
if queued is not None and not queued.result.done():
|
|
420
|
+
queued.result.set_exception(
|
|
421
|
+
SqliteExecutorClosedError(
|
|
422
|
+
"SQLite executor stopped before queued work completed"
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
queued.done.set()
|
|
426
|
+
finally:
|
|
427
|
+
self._write_queue.task_done()
|
|
428
|
+
|
|
429
|
+
writer_task = self._writer_task
|
|
430
|
+
if writer_task is not None:
|
|
431
|
+
writer_task.cancel()
|
|
432
|
+
await asyncio.gather(writer_task, return_exceptions=True)
|
|
433
|
+
self._writer_task = None
|
|
434
|
+
|
|
435
|
+
await self._stop_reader_reaper()
|
|
436
|
+
for connection in tuple(self._read_connections):
|
|
437
|
+
await connection.close()
|
|
438
|
+
self._read_connections.clear()
|
|
439
|
+
await self._writer_connection.close()
|
|
440
|
+
self._state = _ExecutorState.CLOSED
|
|
441
|
+
|
|
442
|
+
def reader(self) -> AbstractAsyncContextManager[SqliteReadSession]:
|
|
443
|
+
self._require_accepting()
|
|
444
|
+
return _ReaderLease(self)
|
|
445
|
+
|
|
446
|
+
async def fetchone(
|
|
447
|
+
self,
|
|
448
|
+
statement: str,
|
|
449
|
+
parameters: Sequence[object] = (),
|
|
450
|
+
) -> aiosqlite.Row | None:
|
|
451
|
+
async with self.reader() as session:
|
|
452
|
+
return await session.fetchone(statement, parameters)
|
|
453
|
+
|
|
454
|
+
async def fetchall(
|
|
455
|
+
self,
|
|
456
|
+
statement: str,
|
|
457
|
+
parameters: Sequence[object] = (),
|
|
458
|
+
) -> list[aiosqlite.Row]:
|
|
459
|
+
async with self.reader() as session:
|
|
460
|
+
return await session.fetchall(statement, parameters)
|
|
461
|
+
|
|
462
|
+
async def execute(
|
|
463
|
+
self,
|
|
464
|
+
statement: str,
|
|
465
|
+
parameters: Sequence[object] = (),
|
|
466
|
+
) -> SqliteExecuteResult:
|
|
467
|
+
return await self._submit(
|
|
468
|
+
lambda session: session.execute(statement, parameters),
|
|
469
|
+
transactional=False,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
async def executemany(
|
|
473
|
+
self,
|
|
474
|
+
statement: str,
|
|
475
|
+
parameter_sets: Iterable[Sequence[object]],
|
|
476
|
+
) -> SqliteExecuteResult:
|
|
477
|
+
frozen_parameters = tuple(tuple(values) for values in parameter_sets)
|
|
478
|
+
return await self._submit(
|
|
479
|
+
lambda session: session.executemany(statement, frozen_parameters),
|
|
480
|
+
transactional=False,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
async def transaction_write(
|
|
484
|
+
self,
|
|
485
|
+
operation: Callable[[SqliteSession], Awaitable[T]],
|
|
486
|
+
) -> T:
|
|
487
|
+
return await self._submit(operation, transactional=True)
|
|
488
|
+
|
|
489
|
+
async def write(
|
|
490
|
+
self,
|
|
491
|
+
operation: Callable[[SqliteSession], Awaitable[T]],
|
|
492
|
+
) -> T:
|
|
493
|
+
return await self._submit(operation, transactional=False)
|
|
494
|
+
|
|
495
|
+
async def _submit(
|
|
496
|
+
self,
|
|
497
|
+
operation: Callable[[SqliteSession], Awaitable[T]],
|
|
498
|
+
*,
|
|
499
|
+
transactional: bool,
|
|
500
|
+
) -> T:
|
|
501
|
+
self._require_accepting()
|
|
502
|
+
request = _WriteRequest(
|
|
503
|
+
operation=operation,
|
|
504
|
+
transactional=transactional,
|
|
505
|
+
result=asyncio.get_running_loop().create_future(),
|
|
506
|
+
)
|
|
507
|
+
await self._write_queue.put(request) # type: ignore[arg-type]
|
|
508
|
+
try:
|
|
509
|
+
return await asyncio.shield(request.result)
|
|
510
|
+
except asyncio.CancelledError:
|
|
511
|
+
state = request.state
|
|
512
|
+
operation_task = (
|
|
513
|
+
state.task if isinstance(state, (_Executing, _Cancelled)) else None
|
|
514
|
+
)
|
|
515
|
+
request.state = _Cancelled(operation_task)
|
|
516
|
+
if operation_task is not None and not operation_task.done():
|
|
517
|
+
operation_task.cancel()
|
|
518
|
+
await self._writer_connection.interrupt()
|
|
519
|
+
await asyncio.shield(request.done.wait())
|
|
520
|
+
raise
|
|
521
|
+
|
|
522
|
+
async def _run_writer(self) -> None:
|
|
523
|
+
session = SqliteSession(self._writer_connection)
|
|
524
|
+
while True:
|
|
525
|
+
request = await self._write_queue.get()
|
|
526
|
+
if request is None:
|
|
527
|
+
self._write_queue.task_done()
|
|
528
|
+
return
|
|
529
|
+
self._current_request = request
|
|
530
|
+
try:
|
|
531
|
+
if isinstance(request.state, _Cancelled):
|
|
532
|
+
if not request.result.done():
|
|
533
|
+
request.result.cancel()
|
|
534
|
+
continue
|
|
535
|
+
operation_task = asyncio.create_task(
|
|
536
|
+
self._run_operation(request, session)
|
|
537
|
+
)
|
|
538
|
+
request.state = _Executing(operation_task)
|
|
539
|
+
try:
|
|
540
|
+
value = await operation_task
|
|
541
|
+
except asyncio.CancelledError as error:
|
|
542
|
+
if not request.result.done():
|
|
543
|
+
if isinstance(request.state, _Cancelled):
|
|
544
|
+
request.result.cancel()
|
|
545
|
+
else:
|
|
546
|
+
request.result.set_exception(error)
|
|
547
|
+
current_task = asyncio.current_task()
|
|
548
|
+
if current_task is not None and current_task.cancelling():
|
|
549
|
+
raise
|
|
550
|
+
except Exception as error: # noqa: BLE001 - preserve operation failures
|
|
551
|
+
if not request.result.done():
|
|
552
|
+
request.result.set_exception(error)
|
|
553
|
+
else:
|
|
554
|
+
if not request.result.done():
|
|
555
|
+
request.result.set_result(value)
|
|
556
|
+
finally:
|
|
557
|
+
self._current_request = None
|
|
558
|
+
request.state = _Finished()
|
|
559
|
+
request.done.set()
|
|
560
|
+
self._write_queue.task_done()
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
async def _run_operation(
|
|
564
|
+
request: _WriteRequest[object],
|
|
565
|
+
session: SqliteSession,
|
|
566
|
+
) -> object:
|
|
567
|
+
if request.transactional:
|
|
568
|
+
async with _WriteTransaction(session):
|
|
569
|
+
return await request.operation(session)
|
|
570
|
+
return await request.operation(session)
|
|
571
|
+
|
|
572
|
+
async def _borrow_reader(self) -> aiosqlite.Connection:
|
|
573
|
+
while True:
|
|
574
|
+
async with self._reader_condition:
|
|
575
|
+
self._require_accepting()
|
|
576
|
+
if self._idle_readers:
|
|
577
|
+
connection = self._idle_readers.pop().connection
|
|
578
|
+
self._borrowed_readers += 1
|
|
579
|
+
return connection
|
|
580
|
+
if (
|
|
581
|
+
self._max_readers is None
|
|
582
|
+
or len(self._read_connections) + self._opening_readers
|
|
583
|
+
< self._max_readers
|
|
584
|
+
):
|
|
585
|
+
self._opening_readers += 1
|
|
586
|
+
break
|
|
587
|
+
await self._reader_condition.wait()
|
|
588
|
+
|
|
589
|
+
try:
|
|
590
|
+
connection = await self._reader_factory()
|
|
591
|
+
except BaseException:
|
|
592
|
+
async with self._reader_condition:
|
|
593
|
+
self._opening_readers -= 1
|
|
594
|
+
self._reader_condition.notify_all()
|
|
595
|
+
raise
|
|
596
|
+
|
|
597
|
+
async with self._reader_condition:
|
|
598
|
+
self._opening_readers -= 1
|
|
599
|
+
if self._state is _ExecutorState.RUNNING:
|
|
600
|
+
self._read_connections.add(connection)
|
|
601
|
+
self._borrowed_readers += 1
|
|
602
|
+
self._reader_condition.notify_all()
|
|
603
|
+
return connection
|
|
604
|
+
self._reader_condition.notify_all()
|
|
605
|
+
await connection.close()
|
|
606
|
+
raise SqliteExecutorClosedError("SQLite executor is not accepting work")
|
|
607
|
+
|
|
608
|
+
async def _return_reader(self, connection: aiosqlite.Connection) -> None:
|
|
609
|
+
async with self._reader_condition:
|
|
610
|
+
self._borrowed_readers -= 1
|
|
611
|
+
if self._state is _ExecutorState.RUNNING:
|
|
612
|
+
self._idle_readers.append(_IdleReader(connection, monotonic()))
|
|
613
|
+
self._reader_condition.notify_all()
|
|
614
|
+
|
|
615
|
+
async def _reap_idle_readers(self) -> None:
|
|
616
|
+
while not self._reader_reaper_stop.is_set():
|
|
617
|
+
try:
|
|
618
|
+
await asyncio.wait_for(
|
|
619
|
+
self._reader_reaper_stop.wait(),
|
|
620
|
+
timeout=self._reader_idle_timeout,
|
|
621
|
+
)
|
|
622
|
+
except TimeoutError:
|
|
623
|
+
pass
|
|
624
|
+
if self._reader_reaper_stop.is_set():
|
|
625
|
+
return
|
|
626
|
+
to_close: list[aiosqlite.Connection] = []
|
|
627
|
+
async with self._reader_condition:
|
|
628
|
+
if self._state is not _ExecutorState.RUNNING:
|
|
629
|
+
return
|
|
630
|
+
excess = max(
|
|
631
|
+
0,
|
|
632
|
+
len(self._read_connections) - self._max_idle_readers,
|
|
633
|
+
)
|
|
634
|
+
cutoff = monotonic() - self._reader_idle_timeout
|
|
635
|
+
while (
|
|
636
|
+
excess > 0
|
|
637
|
+
and self._idle_readers
|
|
638
|
+
and self._idle_readers[0].idle_since <= cutoff
|
|
639
|
+
):
|
|
640
|
+
reader = self._idle_readers.popleft()
|
|
641
|
+
self._read_connections.remove(reader.connection)
|
|
642
|
+
to_close.append(reader.connection)
|
|
643
|
+
excess -= 1
|
|
644
|
+
for connection in to_close:
|
|
645
|
+
try:
|
|
646
|
+
await connection.close()
|
|
647
|
+
except Exception:
|
|
648
|
+
_LOGGER.exception("SQLite idle reader close failed")
|
|
649
|
+
|
|
650
|
+
async def wait_failure(self) -> None:
|
|
651
|
+
await self._failure.wait()
|
|
652
|
+
|
|
653
|
+
async def _stop_reader_reaper(self) -> None:
|
|
654
|
+
task = self._reader_reaper_task
|
|
655
|
+
if task is None:
|
|
656
|
+
return
|
|
657
|
+
self._reader_reaper_stop.set()
|
|
658
|
+
await task
|
|
659
|
+
self._reader_reaper_task = None
|
|
660
|
+
|
|
661
|
+
def _require_accepting(self) -> None:
|
|
662
|
+
if self._state is not _ExecutorState.RUNNING:
|
|
663
|
+
raise SqliteExecutorClosedError("SQLite executor is not accepting work")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
__all__ = [
|
|
667
|
+
"SqliteExecuteResult",
|
|
668
|
+
"SqliteExecutor",
|
|
669
|
+
"SqliteExecutorClosedError",
|
|
670
|
+
"SqliteQueryTimeoutError",
|
|
671
|
+
"SqliteReadSession",
|
|
672
|
+
"SqliteSession",
|
|
673
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PRAGMA busy_timeout = {{ timeout }}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{% from "fragments.sql.tpl" import predicate, expression with context %}
|
|
2
|
+
CREATE {% if operation.index.unique %}UNIQUE {% endif %}INDEX {% if operation.if_not_exists %}IF NOT EXISTS {% endif %}{{ operation.index.name | identifier }}
|
|
3
|
+
ON {{ operation.table | identifier }} (
|
|
4
|
+
{% for column in operation.index.columns %}
|
|
5
|
+
{{ column | identifier }}{{ ", " if not loop.last or operation.index.order_by else "" }}
|
|
6
|
+
{% endfor %}
|
|
7
|
+
{% for sort in operation.index.order_by %}
|
|
8
|
+
{{ expression(sort.expression) }} {{ "DESC" if sort.descending else "ASC" }}{{ ", " if not loop.last else "" }}
|
|
9
|
+
{% endfor %}
|
|
10
|
+
)
|
|
11
|
+
{% if operation.index.where is not none %}
|
|
12
|
+
WHERE {{ predicate(operation.index.where) }}
|
|
13
|
+
{% endif %}
|