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.
Files changed (32) hide show
  1. bazaar_contrib_sqlite/__init__.py +26 -0
  2. bazaar_contrib_sqlite/compiler.py +74 -0
  3. bazaar_contrib_sqlite/database.py +323 -0
  4. bazaar_contrib_sqlite/executor.py +673 -0
  5. bazaar_contrib_sqlite/py.typed +0 -0
  6. bazaar_contrib_sqlite/resources/add_column.sql.tpl +3 -0
  7. bazaar_contrib_sqlite/resources/busy_timeout.sql.tpl +1 -0
  8. bazaar_contrib_sqlite/resources/copy.sql.tpl +3 -0
  9. bazaar_contrib_sqlite/resources/create_index.sql.tpl +13 -0
  10. bazaar_contrib_sqlite/resources/create_table.sql.tpl +21 -0
  11. bazaar_contrib_sqlite/resources/create_trigger.sql.tpl +7 -0
  12. bazaar_contrib_sqlite/resources/delete.sql.tpl +6 -0
  13. bazaar_contrib_sqlite/resources/drop_column.sql.tpl +1 -0
  14. bazaar_contrib_sqlite/resources/drop_index.sql.tpl +1 -0
  15. bazaar_contrib_sqlite/resources/drop_table.sql.tpl +1 -0
  16. bazaar_contrib_sqlite/resources/drop_trigger.sql.tpl +1 -0
  17. bazaar_contrib_sqlite/resources/fragments.sql.tpl +111 -0
  18. bazaar_contrib_sqlite/resources/insert.sql.tpl +23 -0
  19. bazaar_contrib_sqlite/resources/rename_column.sql.tpl +1 -0
  20. bazaar_contrib_sqlite/resources/rename_table.sql.tpl +1 -0
  21. bazaar_contrib_sqlite/resources/search_delete.sql.tpl +8 -0
  22. bazaar_contrib_sqlite/resources/search_insert.sql.tpl +8 -0
  23. bazaar_contrib_sqlite/resources/search_rebuild.sql.tpl +2 -0
  24. bazaar_contrib_sqlite/resources/search_table.sql.tpl +7 -0
  25. bazaar_contrib_sqlite/resources/search_update.sql.tpl +12 -0
  26. bazaar_contrib_sqlite/resources/select.sql.tpl +44 -0
  27. bazaar_contrib_sqlite/resources/update.sql.tpl +10 -0
  28. bazaar_contrib_sqlite-0.3.0rc1.dist-info/METADATA +35 -0
  29. bazaar_contrib_sqlite-0.3.0rc1.dist-info/RECORD +32 -0
  30. bazaar_contrib_sqlite-0.3.0rc1.dist-info/WHEEL +4 -0
  31. bazaar_contrib_sqlite-0.3.0rc1.dist-info/entry_points.txt +3 -0
  32. 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,3 @@
1
+ {% from "fragments.sql.tpl" import column_definition with context %}
2
+ ALTER TABLE {{ operation.table | identifier }}
3
+ ADD COLUMN {{ column_definition(operation.column) }}
@@ -0,0 +1 @@
1
+ PRAGMA busy_timeout = {{ timeout }}
@@ -0,0 +1,3 @@
1
+ INSERT INTO {{ request.table | identifier }} ({{ request.query.columns | map("identifier") | join(", ") }})
2
+ {% set request = request.query %}
3
+ {% include "select.sql.tpl" %}
@@ -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 %}