beaver-db 2.4.0__tar.gz → 2.4.2__tar.gz

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 (205) hide show
  1. {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/ci.yml +3 -0
  2. beaver_db-2.4.2/.github/workflows/pages.yml +62 -0
  3. {beaver_db-2.4.0 → beaver_db-2.4.2}/.gitignore +1 -0
  4. {beaver_db-2.4.0 → beaver_db-2.4.2}/CHANGELOG.md +47 -0
  5. {beaver_db-2.4.0 → beaver_db-2.4.2}/PKG-INFO +6 -13
  6. {beaver_db-2.4.0 → beaver_db-2.4.2}/README.md +4 -11
  7. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/__init__.py +1 -1
  8. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/blobs.py +3 -3
  9. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/core.py +73 -20
  10. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/dicts.py +15 -10
  11. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/docs.py +42 -16
  12. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/lists.py +2 -2
  13. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/locks.py +1 -1
  14. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/manager.py +18 -0
  15. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/queues.py +2 -2
  16. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/vectors.py +2 -2
  17. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/_quarto.yml +12 -5
  18. beaver_db-2.4.2/docs/beaver.scss +29 -0
  19. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-concurrency.md +4 -21
  20. beaver_db-2.4.2/docs/img/beaver.webp +0 -0
  21. {beaver_db-2.4.0 → beaver_db-2.4.2}/makefile +11 -1
  22. {beaver_db-2.4.0 → beaver_db-2.4.2}/pyproject.toml +1 -1
  23. beaver_db-2.4.2/site/build.py +76 -0
  24. beaver_db-2.4.2/site/check_snippets.py +61 -0
  25. beaver_db-2.4.2/site/fonts/OFL-inter.txt +93 -0
  26. beaver_db-2.4.2/site/fonts/OFL-jetbrainsmono.txt +93 -0
  27. beaver_db-2.4.2/site/fonts/OFL-nunito.txt +93 -0
  28. beaver_db-2.4.2/site/fonts/inter-400.woff2 +0 -0
  29. beaver_db-2.4.2/site/fonts/inter-500.woff2 +0 -0
  30. beaver_db-2.4.2/site/fonts/inter-600.woff2 +0 -0
  31. beaver_db-2.4.2/site/fonts/jbmono-400.woff2 +0 -0
  32. beaver_db-2.4.2/site/fonts/jbmono-500.woff2 +0 -0
  33. beaver_db-2.4.2/site/fonts/nunito-800.woff2 +0 -0
  34. beaver_db-2.4.2/site/img/beaver-180.png +0 -0
  35. beaver_db-2.4.2/site/img/beaver-32.png +0 -0
  36. beaver_db-2.4.2/site/img/beaver-96.webp +0 -0
  37. beaver_db-2.4.2/site/img/logo-400.webp +0 -0
  38. beaver_db-2.4.2/site/index.html +209 -0
  39. beaver_db-2.4.2/site/redirects.txt +24 -0
  40. beaver_db-2.4.2/site/site.css +148 -0
  41. beaver_db-2.4.2/site/site.js +69 -0
  42. beaver_db-2.4.2/tests/unit/test_release_2_4_2.py +122 -0
  43. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_transactions.py +67 -6
  44. {beaver_db-2.4.0 → beaver_db-2.4.2}/uv.lock +1 -1
  45. beaver_db-2.4.0/docs/index.html +0 -620
  46. {beaver_db-2.4.0 → beaver_db-2.4.2}/.dockerignore +0 -0
  47. {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/release.yaml +0 -0
  48. {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/tests.yaml +0 -0
  49. {beaver_db-2.4.0 → beaver_db-2.4.2}/.python-version +0 -0
  50. {beaver_db-2.4.0 → beaver_db-2.4.2}/.vscode/settings.json +0 -0
  51. {beaver_db-2.4.0 → beaver_db-2.4.2}/AGENTS.md +0 -0
  52. {beaver_db-2.4.0 → beaver_db-2.4.2}/LICENSE +0 -0
  53. {beaver_db-2.4.0 → beaver_db-2.4.2}/STATUS.md +0 -0
  54. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/_fracdex.py +0 -0
  55. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/api.py +0 -0
  56. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/bridge.py +0 -0
  57. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cache.py +0 -0
  58. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/channels.py +0 -0
  59. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/__init__.py +0 -0
  60. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/discovery.py +0 -0
  61. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/main.py +0 -0
  62. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/client.py +0 -0
  63. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/errors.py +0 -0
  64. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/events.py +0 -0
  65. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/graphs.py +0 -0
  66. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/indexing.py +0 -0
  67. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/interfaces.py +0 -0
  68. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/logs.py +0 -0
  69. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/migrate.py +0 -0
  70. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/queries.py +0 -0
  71. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/security.py +0 -0
  72. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/server.py +0 -0
  73. {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/sketches.py +0 -0
  74. {beaver_db-2.4.0 → beaver_db-2.4.2}/design.md +0 -0
  75. {beaver_db-2.4.0 → beaver_db-2.4.2}/dockerfile +0 -0
  76. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/.gitignore +0 -0
  77. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-architecture.md +0 -0
  78. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-contributing.md +0 -0
  79. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-search.md +0 -0
  80. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-blobs.md +0 -0
  81. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-channels.md +0 -0
  82. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-collections.md +0 -0
  83. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-deployment.md +0 -0
  84. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-dicts.md +0 -0
  85. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-lists.md +0 -0
  86. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-locks.md +0 -0
  87. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-logs.md +0 -0
  88. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-queues.md +0 -0
  89. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-sketches.md +0 -0
  90. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide.md +0 -0
  91. /beaver_db-2.4.0/docs/intro.md → /beaver_db-2.4.2/docs/index.md +0 -0
  92. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/logo.png +0 -0
  93. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/migration-1x-to-2x.md +0 -0
  94. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/migration-rc3-to-rc4-lists.md +0 -0
  95. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/quickstart.md +0 -0
  96. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/superpowers/plans/2026-05-24-list-fracdex-ordering.md +0 -0
  97. {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/superpowers/specs/2026-05-24-list-fracdex-ordering-design.md +0 -0
  98. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/async_pubsub.py +0 -0
  99. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/blobs.py +0 -0
  100. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/cache.py +0 -0
  101. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/fts.py +0 -0
  102. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/fuzzy.py +0 -0
  103. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/general_test.py +0 -0
  104. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/graph.py +0 -0
  105. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/kvstore.py +0 -0
  106. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/list.py +0 -0
  107. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/locks.py +0 -0
  108. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/logs.py +0 -0
  109. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/pqueue.py +0 -0
  110. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/producer_consumer.py +0 -0
  111. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/publisher.py +0 -0
  112. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/pubsub.py +0 -0
  113. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/rerank.py +0 -0
  114. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/stress_vectors.py +0 -0
  115. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/subscriber.py +0 -0
  116. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/textual_chat.css +0 -0
  117. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/textual_chat.py +0 -0
  118. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/type_hints.py +0 -0
  119. {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/vector.py +0 -0
  120. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/15-enhance-cli-with-admin-commands-shell-piping-and-interactivity.md +0 -0
  121. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/18-enhanced-dump-and-load-for-etl.md +0 -0
  122. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/19-add-comprehensive-unit-integration-and-concurrency-test-suite.md +0 -0
  123. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/24-refactor-vectorindex-to-be-a-swappable-strategy-and-add-persistent-lsh-implementation.md +0 -0
  124. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/27-batched-operations-api-batched.md +0 -0
  125. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/28-add-hnsw-vector-strategy-beaver-dbhnsw.md +0 -0
  126. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/36-implement-sid-consumers-dynamic-cli-universal-client-server.md +0 -0
  127. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/37-v2-release-phase-0-audit-and-stabilize.md +0 -0
  128. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/38-v2-release-phase-1-storage-engine.md +0 -0
  129. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/39-v2-release-phase-2-wrappers.md +0 -0
  130. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/40-batched-reentry-deadlock-under-sync-portal-on-large-sequential-writes.md +0 -0
  131. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/41-2x-silently-opens-a-1x-database-and-goes-split-brain.md +0 -0
  132. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/42-uniform-field-indexing-across-collections.md +0 -0
  133. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/43-singleton-silently-ignores-differing-kwargs-on-a-cache-hit.md +0 -0
  134. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/44-filters-have-no-cli-or-wire-encoding-so-explain-cannot-be-exposed.md +0 -0
  135. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
  136. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/10-expose-dblock-functionality-on-all-high-level-data-managers.md +0 -0
  137. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/12-add-dump-method-for-json-export-to-all-data-managers.md +0 -0
  138. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/13-adopt-pydantic-deprecate-beavermodel-and-refactor-document-to-be-generic.md +0 -0
  139. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/14-deprecate-fire-based-cli-and-build-a-feature-rich-typer-and-rich-cli.md +0 -0
  140. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/16-add-clear-method-for-all-data-structures.md +0 -0
  141. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/17-replace-atomic-operations-with-custom-beaver-lock.md +0 -0
  142. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/2-comprehensive-async-wrappers.md +0 -0
  143. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/21-implement-dbcache-property-with-wal-invalidation-dummy-fallback-and-performance-stats.md +0 -0
  144. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/22-add-context-manager-support-and-robust-process-wide-cleanup-to-beaverdb.md +0 -0
  145. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/23-global-event-listeners-api.md +0 -0
  146. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/25-refactor-core-to-async-first-architecture-with-sync-bridge.md +0 -0
  147. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/30-add-probabilistic-sketches-approximate-sets-counters.md +0 -0
  148. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/32-secure-dictionaries-encryption-and-hashing.md +0 -0
  149. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/34-composable-graph-algorithms-query-builder-graphrag.md +0 -0
  150. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/35-refactor-core-to-shared-interface-definition-sid-architecture.md +0 -0
  151. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
  152. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
  153. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
  154. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
  155. {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
  156. {beaver_db-2.4.0 → beaver_db-2.4.2}/logo.png +0 -0
  157. {beaver_db-2.4.0 → beaver_db-2.4.2}/plans/2026-08-01-field-indexing-slices-1-2.md +0 -0
  158. {beaver_db-2.4.0 → beaver_db-2.4.2}/pytest.ini +0 -0
  159. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/benchmark_lsh.py +0 -0
  160. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/concurrency/conftest.py +0 -0
  161. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/concurrency/test_concurrency.py +0 -0
  162. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/conftest.py +0 -0
  163. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/__init__.py +0 -0
  164. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/test_server_subprocess.py +0 -0
  165. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/test_sync.py +0 -0
  166. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/perftest.py +0 -0
  167. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/test_docs_search_filters.py +0 -0
  168. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_api_expose.py +0 -0
  169. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_batched.py +0 -0
  170. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_blobs.py +0 -0
  171. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_bridge.py +0 -0
  172. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_channels.py +0 -0
  173. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_cli.py +0 -0
  174. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_local_only.py +0 -0
  175. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip.py +0 -0
  176. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_lists.py +0 -0
  177. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_logs.py +0 -0
  178. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_queues.py +0 -0
  179. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_connect.py +0 -0
  180. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_db_version.py +0 -0
  181. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_dicts.py +0 -0
  182. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_dicts_decorated.py +0 -0
  183. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_docs.py +0 -0
  184. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_errors.py +0 -0
  185. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_events.py +0 -0
  186. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_field_indexing.py +0 -0
  187. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_fracdex.py +0 -0
  188. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_graphs.py +0 -0
  189. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists.py +0 -0
  190. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists_decorated.py +0 -0
  191. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists_fracdex.py +0 -0
  192. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_locks.py +0 -0
  193. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_logs.py +0 -0
  194. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_logs_decorated.py +0 -0
  195. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lsh.py +0 -0
  196. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_manager_events.py +0 -0
  197. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_migrate.py +0 -0
  198. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_queues.py +0 -0
  199. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_queues_decorated.py +0 -0
  200. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_security.py +0 -0
  201. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_auth.py +0 -0
  202. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_errors.py +0 -0
  203. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_routes.py +0 -0
  204. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_sketchs.py +0 -0
  205. {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_vectors.py +0 -0
@@ -25,3 +25,6 @@ jobs:
25
25
 
26
26
  - name: Run CI (format-check + unit tests)
27
27
  run: make ci
28
+
29
+ - name: Check the snippets on the product page
30
+ run: uv run python site/check_snippets.py
@@ -0,0 +1,62 @@
1
+ name: pages
2
+ # beaver.syalia.com: the product page in site/ at the root, the Quarto docs
3
+ # under /docs/, and a redirect page for each old docs URL. One artifact,
4
+ # assembled by site/build.py. Pull requests build it; main deploys it.
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ paths: ["site/**", "docs/**", "pyproject.toml", ".github/workflows/pages.yml"]
9
+ pull_request:
10
+ branches: [main]
11
+ paths: ["site/**", "docs/**", "pyproject.toml", ".github/workflows/pages.yml"]
12
+ workflow_dispatch:
13
+ permissions:
14
+ contents: read
15
+ pages: write
16
+ id-token: write
17
+ concurrency:
18
+ group: pages-${{ github.ref }}
19
+ cancel-in-progress: true
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ outputs:
24
+ build_type: ${{ steps.pages.outputs.build_type }}
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.12"
30
+ - uses: quarto-dev/quarto-actions/setup@v2
31
+ with:
32
+ version: 1.9.37
33
+ - name: Render the docs
34
+ run: quarto render docs
35
+ - name: Assemble the site
36
+ run: python site/build.py public
37
+ - uses: actions/upload-pages-artifact@v3
38
+ with:
39
+ path: public
40
+ # Deploying needs the repository's Pages source set to "GitHub Actions".
41
+ # While it is still the gh-pages branch, the deploy job is skipped and
42
+ # the old site stays up; re-run this workflow after switching.
43
+ - id: pages
44
+ name: Read the Pages source
45
+ env:
46
+ GH_TOKEN: ${{ github.token }}
47
+ run: |
48
+ build_type=$(gh api "repos/${{ github.repository }}/pages" --jq .build_type || echo none)
49
+ echo "build_type=$build_type" >> "$GITHUB_OUTPUT"
50
+ if [ "$build_type" != workflow ]; then
51
+ echo "::notice::Pages source is '$build_type', not 'workflow': the deploy job will be skipped."
52
+ fi
53
+ deploy:
54
+ needs: build
55
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.build.outputs.build_type == 'workflow'
56
+ runs-on: ubuntu-latest
57
+ environment:
58
+ name: github-pages
59
+ url: ${{ steps.deploy.outputs.page_url }}
60
+ steps:
61
+ - id: deploy
62
+ uses: actions/deploy-pages@v4
@@ -14,3 +14,4 @@ wheels/
14
14
  .coverage
15
15
  _site
16
16
  benchmark
17
+ public/
@@ -2,6 +2,53 @@
2
2
 
3
3
  All notable changes to beaver-db will be recorded here.
4
4
 
5
+ ## 2.4.2 — 2026-10-30
6
+
7
+ ### Fixed
8
+
9
+ - **`db.lock()` could admit two waiters at once (#38).** The queue was ordered by a timestamp taken before the waiter's insert, so a waiter that stamped first and inserted second sorted ahead of the holder. The queue is now ordered by `rowid`, the order of arrival. A read-increment-write loop under the lock, 24 increments from 8 threads over two objects on one file, gave a duplicate in 2 of 30 runs on 2.4.1 and in 0 of 100 on 2.4.2.
10
+ - **Indexing documents in several batches was quadratic (#40).** Each document's delete scanned the whole FTS5 table. It now finds the rows through a `MATCH` on the id. Three batches of 5,000 short documents took 0.3 s, 25.7 s and 50.4 s on 2.4.1, and about 0.3 s each on 2.4.2. Ids with no tokens, such as `___`, still take the scan.
11
+ - **An expired `dict` key was not removed when read.** The delete ran inside the transaction that the `KeyError` rolled back, so the row stayed until it was overwritten.
12
+
13
+ ### Changed
14
+
15
+ - **Reads no longer write.** `dict.get`, `list.get`, `queue.peek`, `docs.get`, `vectors.get`, `blob.get` and `blob.fetch` took the manager's internal lock, which writes a row to acquire and another to release. They now take only a transaction. `dict.get` went from about 200 to about 700 calls per second through `BeaverDB`.
16
+ - **Several processes writing to one manager wait less.** The internal lock polls every 5 ms instead of every 100 ms. Two processes writing to one `dict` went from 22 to 156 writes per second, and four from 21 to 34.
17
+
18
+ ### Corrected
19
+
20
+ - The 2.4.0 entry said Starlette and uvicorn cancel a request task whenever a client disconnects. In our measurement 12,800 client aborts against a fast plain handler caused no cancellation, while a streaming response was cancelled on every disconnect. The fix stands, and what started the 2026-08-10 outage is not known.
21
+
22
+ ## 2.4.1 — 2026-08-10
23
+
24
+ ### Fixed
25
+
26
+ - **2.4.0's cleanup could itself hang, and could roll back the wrong
27
+ transaction.** Both were found by exercising a real service over HTTP rather
28
+ than in a unit test, and both come from the same fact: aiosqlite serialises
29
+ every statement onto one worker thread.
30
+
31
+ * *The hang.* When a **separate connection** holds the write lock, the
32
+ worker thread is parked inside SQLite's busy handler with `BEGIN
33
+ IMMEDIATE` still running. 2.4.0 *awaited* its cleanup rollback, which
34
+ queued behind that stuck statement — so a caller's 1.5 s deadline became a
35
+ 30 s wait for SQLite's busy-timeout. Measured on warden's `/healthz`: curl
36
+ sat through the full 10 s of its `--max-time` and returned nothing.
37
+ * *The wrong rollback.* Merely scheduling it instead is not enough. An
38
+ uncontended `asyncio.Lock.acquire()` does not yield, so the next
39
+ transaction in the same task enqueues its `BEGIN` **ahead** of the pending
40
+ `ROLLBACK` — which then rolls back that innocent transaction, silently
41
+ committing its writes in autocommit.
42
+
43
+ Cleanup now runs in a background task that **holds the transaction lock
44
+ until the rollback has landed**. The caller returns immediately, and
45
+ ordering comes from mutual exclusion rather than from scheduling order.
46
+ Waiters are bounded by the same `connection_timeout` as before.
47
+
48
+ - **Reclaiming an orphaned transaction awaits its rollback**, for the same
49
+ ordering reason — that path continues straight to `BEGIN` without yielding.
50
+ Bounded, and a timeout there raises `TransactionTimeoutError`.
51
+
5
52
  ## 2.4.0 — 2026-08-10
6
53
 
7
54
  ### Fixed — availability class
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: beaver-db
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Fast, async-native, embedded, and multi-modal DB based on SQLite for AI-powered applications.
5
5
  License-File: LICENSE
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -61,13 +61,6 @@ Install the core library:
61
61
  pip install beaver-db
62
62
  ```
63
63
 
64
- > **Note:** `beaver-db 2.0` is currently in release-candidate cycle. The
65
- > CLI (`beaver` command), REST API server (`beaver serve`), `BeaverClient`
66
- > remote, and the published Docker image are all **landing in 2.0 final**
67
- > (target: June 2026). Today the library is consumed as a Python API only.
68
- > Track progress at `vault/Atlas/Architecture/2026-05-15-beaver-v2-release-plan.md`
69
- > in the workspace, or watch the `2.0` issues on GitHub.
70
-
71
64
  ## Quickstart
72
65
 
73
66
  Get up and running in 30 seconds. This example showcases a dictionary, a list, and full-text search in a single script.
@@ -113,14 +106,14 @@ db.close()
113
106
  * [**Persistent Priority Queue**](https://syalia.com/beaver/guide-queues.html): A high-performance, persistent priority queue perfect for task orchestration across multiple processes.
114
107
  * [**Probabilistic Sketches:**]() Track cardinality and membership for millions of items in constant space using HyperLogLog and Bloom Filters.
115
108
  * [**Document Collections**](https://syalia.com/beaver/guide-collections.html): Store rich documents combining a vector embedding and Pydantic-based metadata.
116
- * [**Vector Search**](https://syalia.com/beaver/guide-collections.html%23vector-search): Fast, multi-process-safe linear vector search using an in-memory `numpy`-based index.
117
- * [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html%23full-text-fuzzy-search): Automatically index and search through document metadata using SQLite's FTS5 engine, with optional fuzzy search for typo-tolerant matching.
118
- * [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html%23knowledge-graph): Create directed, labeled relationships between documents and traverse the graph to find neighbors or perform multi-hop walks.
109
+ * [**Vector Search**](https://syalia.com/beaver/guide-collections.html#vector-search-semantic): Fast, multi-process-safe linear vector search using an in-memory `numpy`-based index.
110
+ * [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html#full-text-search-fts): Automatically index and search through document metadata using SQLite's FTS5 engine, with optional fuzzy search for typo-tolerant matching.
111
+ * [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html#graph-relationships): Create directed, labeled relationships between documents and traverse the graph to find neighbors or perform multi-hop walks.
119
112
  * [**Pub/Sub System**](https://syalia.com/beaver/guide-channels.html): A powerful, thread and process-safe publish-subscribe system for real-time messaging with a fan-out architecture.
120
113
  * [**Time-Indexed Logs**](https://syalia.com/beaver/guide-logs.html): A specialized data structure for structured, time-series logs. Query historical data by time range or create a live, aggregated view.
121
114
  * [**Event-Driven Callbacks**](https://syalia.com/beaver/guide-channels.html): Listen for database changes in real-time. Subscribe to events on specific managers to trigger workflows or update UIs.
122
115
  * [**Inter-Process Locking**](https://syalia.com/beaver/guide-locks.html): Robust, deadlock-proof locks. Use `db.lock('task_name')` to coordinate arbitrary scripts, or `with db.list('my_list') as l:` to perform atomic, multi-step operations.
123
- * [**Pydantic Support**](https://syalia.com/beaver/dev-architecture.html%23type-safe-models): Optionally associate `pydantic.BaseModel`s with any data structure for automatic, recursive data validation and (de)serialization.
116
+ * [**Pydantic Support**](https://syalia.com/beaver/guide.html#type-safe-models-with-pydantic): Optionally associate `pydantic.BaseModel`s with any data structure for automatic, recursive data validation and (de)serialization.
124
117
  * **Data Export**: Dump most data structures to a portable JSON file with a single `.dump()` command. (Full backup/restore symmetry — `.load()` across all managers, plus the `beaver` CLI and REST server — lands in `2.0` final; tracked in issues #15, #18, #36.)
125
118
 
126
119
  ## Documentation
@@ -34,13 +34,6 @@ Install the core library:
34
34
  pip install beaver-db
35
35
  ```
36
36
 
37
- > **Note:** `beaver-db 2.0` is currently in release-candidate cycle. The
38
- > CLI (`beaver` command), REST API server (`beaver serve`), `BeaverClient`
39
- > remote, and the published Docker image are all **landing in 2.0 final**
40
- > (target: June 2026). Today the library is consumed as a Python API only.
41
- > Track progress at `vault/Atlas/Architecture/2026-05-15-beaver-v2-release-plan.md`
42
- > in the workspace, or watch the `2.0` issues on GitHub.
43
-
44
37
  ## Quickstart
45
38
 
46
39
  Get up and running in 30 seconds. This example showcases a dictionary, a list, and full-text search in a single script.
@@ -86,14 +79,14 @@ db.close()
86
79
  * [**Persistent Priority Queue**](https://syalia.com/beaver/guide-queues.html): A high-performance, persistent priority queue perfect for task orchestration across multiple processes.
87
80
  * [**Probabilistic Sketches:**]() Track cardinality and membership for millions of items in constant space using HyperLogLog and Bloom Filters.
88
81
  * [**Document Collections**](https://syalia.com/beaver/guide-collections.html): Store rich documents combining a vector embedding and Pydantic-based metadata.
89
- * [**Vector Search**](https://syalia.com/beaver/guide-collections.html%23vector-search): Fast, multi-process-safe linear vector search using an in-memory `numpy`-based index.
90
- * [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html%23full-text-fuzzy-search): Automatically index and search through document metadata using SQLite's FTS5 engine, with optional fuzzy search for typo-tolerant matching.
91
- * [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html%23knowledge-graph): Create directed, labeled relationships between documents and traverse the graph to find neighbors or perform multi-hop walks.
82
+ * [**Vector Search**](https://syalia.com/beaver/guide-collections.html#vector-search-semantic): Fast, multi-process-safe linear vector search using an in-memory `numpy`-based index.
83
+ * [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html#full-text-search-fts): Automatically index and search through document metadata using SQLite's FTS5 engine, with optional fuzzy search for typo-tolerant matching.
84
+ * [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html#graph-relationships): Create directed, labeled relationships between documents and traverse the graph to find neighbors or perform multi-hop walks.
92
85
  * [**Pub/Sub System**](https://syalia.com/beaver/guide-channels.html): A powerful, thread and process-safe publish-subscribe system for real-time messaging with a fan-out architecture.
93
86
  * [**Time-Indexed Logs**](https://syalia.com/beaver/guide-logs.html): A specialized data structure for structured, time-series logs. Query historical data by time range or create a live, aggregated view.
94
87
  * [**Event-Driven Callbacks**](https://syalia.com/beaver/guide-channels.html): Listen for database changes in real-time. Subscribe to events on specific managers to trigger workflows or update UIs.
95
88
  * [**Inter-Process Locking**](https://syalia.com/beaver/guide-locks.html): Robust, deadlock-proof locks. Use `db.lock('task_name')` to coordinate arbitrary scripts, or `with db.list('my_list') as l:` to perform atomic, multi-step operations.
96
- * [**Pydantic Support**](https://syalia.com/beaver/dev-architecture.html%23type-safe-models): Optionally associate `pydantic.BaseModel`s with any data structure for automatic, recursive data validation and (de)serialization.
89
+ * [**Pydantic Support**](https://syalia.com/beaver/guide.html#type-safe-models-with-pydantic): Optionally associate `pydantic.BaseModel`s with any data structure for automatic, recursive data validation and (de)serialization.
97
90
  * **Data Export**: Dump most data structures to a portable JSON file with a single `.dump()` command. (Full backup/restore symmetry — `.load()` across all managers, plus the `beaver` CLI and REST server — lands in `2.0` final; tracked in issues #15, #18, #36.)
98
91
 
99
92
  ## Documentation
@@ -11,7 +11,7 @@ from .queries import q
11
11
  from .security import Secret
12
12
  from .errors import BeaverRemoteError, LocalOnlyError
13
13
 
14
- __version__ = "2.4.0"
14
+ __version__ = "2.4.2"
15
15
 
16
16
 
17
17
  def connect(source: str, *, sync: bool = True, api_key: str | None = None):
@@ -7,7 +7,7 @@ from typing import (
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
- from .manager import AsyncBeaverBase, atomic, emits
10
+ from .manager import AsyncBeaverBase, atomic, read_atomic, emits
11
11
  from .interfaces import IAsyncBeaverBlob, BlobItem
12
12
 
13
13
 
@@ -75,7 +75,7 @@ class AsyncBeaverBlob[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverBlob[T]):
75
75
  (self._name, key, data, meta_json),
76
76
  )
77
77
 
78
- @atomic
78
+ @read_atomic
79
79
  async def fetch(self, key: str) -> BlobItem:
80
80
  """
81
81
  Retrieves the full BlobItem (data + metadata).
@@ -93,7 +93,7 @@ class AsyncBeaverBlob[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverBlob[T]):
93
93
  meta = json.loads(row["metadata"]) if row["metadata"] else None
94
94
  return BlobItem(key=key, data=row["data"], metadata=meta)
95
95
 
96
- @atomic
96
+ @read_atomic
97
97
  async def get(self, key: str) -> bytes:
98
98
  """
99
99
  Retrieves just the binary data for a key.
@@ -137,10 +137,7 @@ class Transaction:
137
137
  try:
138
138
  await self.db.connection.execute("BEGIN IMMEDIATE")
139
139
  except BaseException:
140
- try:
141
- await self._rollback_quietly()
142
- finally:
143
- self.db._tx_lock.release()
140
+ self._clean_up_in_the_background()
144
141
  raise
145
142
 
146
143
  self.db._tx_owner_task = current_task
@@ -206,28 +203,81 @@ class Transaction:
206
203
  self.db._db_path,
207
204
  owner.get_name(),
208
205
  )
209
- await self._rollback_quietly()
210
206
  self.db._tx_owner_task = None
207
+
208
+ # Awaited, unlike the cleanup on the failure path below. There, the
209
+ # release()/wake-up of the next waiter is itself scheduled *after* the
210
+ # rollback task, so the queue order works out on its own. Here the same
211
+ # coroutine goes straight on to BEGIN IMMEDIATE without yielding, so a
212
+ # merely-scheduled rollback would land behind it — and SQLite refuses
213
+ # to begin a transaction inside one.
214
+ #
215
+ # Bounded, because the orphan's own transaction did complete its BEGIN,
216
+ # so the worker thread should be free; if it is not, something worse is
217
+ # wrong and the caller deserves to hear about it rather than wait.
218
+ try:
219
+ async with asyncio.timeout(self._timeout):
220
+ await self.db.connection.rollback()
221
+ except TimeoutError:
222
+ self.db._tx_lock.release()
223
+ raise TransactionTimeoutError(
224
+ f"could not roll back an orphaned transaction on "
225
+ f"{self.db._db_path!r} within {self._timeout:g}s — the "
226
+ f"connection is not responding"
227
+ ) from None
228
+ except Exception:
229
+ pass # nothing was open after all; the lock is ours either way
211
230
  return True
212
231
 
213
- async def _rollback_quietly(self) -> None:
214
- """Undo a BEGIN that may have taken effect even though its await did not.
232
+ def _clean_up_in_the_background(self) -> None:
233
+ """Undo a BEGIN that failed, and release the lock once it is undone.
234
+
235
+ A BEGIN can fail two ways, and they need opposite things:
236
+
237
+ * **It raised.** Nothing began; the rollback is a formality.
238
+ * **Its await was cancelled.** `connection.execute("BEGIN IMMEDIATE")`
239
+ is two things — the statement running on aiosqlite's worker thread,
240
+ and the future we wait on. Cancelling the wait does not un-run the
241
+ statement, so SQLite's write lock may now be held with nothing left
242
+ to release it. That is the whole outage this class exists to prevent.
243
+
244
+ We cannot tell which happened, so we always roll back. What we must not
245
+ do is **await** it here: the worker thread may be parked inside
246
+ SQLite's busy handler because another *connection* holds the write
247
+ lock, and aiosqlite serialises one queue onto that thread — so the
248
+ rollback would sit behind the very statement that is stuck, turning a
249
+ caller's 1.5 s deadline into a 30 s hang. (Measured: warden's /healthz
250
+ sat through the full 10 s of curl's `--max-time` exactly this way.)
251
+
252
+ So the caller leaves immediately and a background task finishes the
253
+ job. **It holds `_tx_lock` until the rollback lands**, which is what
254
+ makes the ordering safe: without that, an uncontended `Lock.acquire()`
255
+ does not yield, so the very next transaction in this task would enqueue
256
+ its BEGIN *ahead* of our pending ROLLBACK — and get rolled back by it.
257
+ Mutual exclusion orders the two; scheduling luck does not. Anyone
258
+ waiting meanwhile is bounded by `_acquire`, and a store this wedged
259
+ genuinely is unavailable.
260
+ """
215
261
 
216
- `await connection.execute("BEGIN IMMEDIATE")` is two things: the
217
- statement running on aiosqlite's worker thread, and the future we wait
218
- on. Cancelling the wait does not un-run the statement — so a cancelled
219
- BEGIN can leave SQLite's write lock held with nothing left to release
220
- it. That is the whole outage.
262
+ async def clean_up():
263
+ try:
264
+ await self.db.connection.rollback()
265
+ except Exception as exc:
266
+ # Expected when the BEGIN never took: there is no transaction
267
+ # to undo. The caller already has its own exception.
268
+ _log.debug("rollback of an abandoned transaction failed: %r", exc)
269
+ finally:
270
+ self.db._tx_lock.release()
221
271
 
222
- Shielded, because the rollback has to survive the very cancellation
223
- that made it necessary. Errors are swallowed: this runs while another
224
- exception is already propagating, and `rollback()` is a no-op when no
225
- transaction turned out to be open.
226
- """
227
272
  try:
228
- await asyncio.shield(self.db.connection.rollback())
229
- except Exception:
230
- pass
273
+ task = asyncio.ensure_future(clean_up())
274
+ except RuntimeError: # no running loop left to clean up on
275
+ self.db._tx_lock.release()
276
+ return
277
+ # Hold a reference: a bare ensure_future() can be garbage-collected
278
+ # mid-flight, and this one is holding the write lock.
279
+ self.db._pending_rollbacks.add(task)
280
+ task.add_done_callback(self.db._pending_rollbacks.discard)
231
281
 
232
282
  async def __aexit__(self, exc_type, exc, tb):
233
283
  # Only the root transaction performs the Commit/Rollback and Release
@@ -280,6 +330,9 @@ class AsyncBeaverDB:
280
330
  # Locking Primitives
281
331
  self._tx_lock = asyncio.Lock()
282
332
  self._tx_owner_task: asyncio.Task | None = None # Track owner for reentrancy
333
+ # In-flight cleanup rollbacks (see Transaction._rollback_quietly), held
334
+ # only so the event loop cannot garbage-collect them mid-flight.
335
+ self._pending_rollbacks: set[asyncio.Task] = set()
283
336
 
284
337
  # Manager Singleton Cache
285
338
  self._manager_cache: dict[tuple[type, str], Any] = {}
@@ -210,17 +210,17 @@ class AsyncBeaverDict[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverDict[T]):
210
210
  @expose(
211
211
  path="/{key}", method="GET", cli_name="get", cli_help="Get the value for a key."
212
212
  )
213
- @atomic
214
213
  async def get(self, key: str) -> T:
215
214
  """Retrieves a value for a key. Raises KeyError if missing or expired."""
216
215
  if self._secret_arg and not self._cipher:
217
216
  await self._setup_security(self._secret_arg)
218
217
 
219
- cursor = await self.connection.execute(
220
- "SELECT value, expires_at FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
221
- (self._name, key),
222
- )
223
- result = await cursor.fetchone()
218
+ async with self._db.transaction():
219
+ cursor = await self.connection.execute(
220
+ "SELECT value, expires_at FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
221
+ (self._name, key),
222
+ )
223
+ result = await cursor.fetchone()
224
224
 
225
225
  if result is None:
226
226
  raise KeyError(f"Key '{key}' not found in dictionary '{self._name}'")
@@ -228,16 +228,21 @@ class AsyncBeaverDict[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverDict[T]):
228
228
  raw_value, expires_at = result["value"], result["expires_at"]
229
229
 
230
230
  if expires_at is not None and time.time() > expires_at:
231
- await self.connection.execute(
232
- "DELETE FROM __beaver_dicts__ WHERE dict_name = ? AND key = ?",
233
- (self._name, key),
234
- )
231
+ # Outside the read's transaction: the KeyError would roll this back.
232
+ await self._delete_expired(key)
235
233
  raise KeyError(
236
234
  f"Key '{key}' not found in dictionary '{self._name}' (expired)"
237
235
  )
238
236
 
239
237
  return self._deserialize(raw_value)
240
238
 
239
+ @atomic
240
+ async def _delete_expired(self, key: str):
241
+ await self.connection.execute(
242
+ "DELETE FROM __beaver_dicts__ WHERE dict_name = ? AND key = ? AND expires_at < ?",
243
+ (self._name, key, time.time()),
244
+ )
245
+
241
246
  @expose(path="/{key}", method="DELETE", cli_name="delete", cli_help="Delete a key.")
242
247
  @emits("del", payload=lambda key, *args, **kwargs: dict(key=key))
243
248
  @atomic
@@ -13,7 +13,7 @@ from pydantic import BaseModel
13
13
 
14
14
  from . import indexing
15
15
  from .queries import Filter
16
- from .manager import AsyncBeaverBase, atomic, emits
16
+ from .manager import AsyncBeaverBase, atomic, read_atomic, emits
17
17
  from .interfaces import Document, ScoredDocument, IDocumentQuery
18
18
 
19
19
 
@@ -106,6 +106,41 @@ class DocumentQuery[T: BaseModel](IDocumentQuery[T]):
106
106
  yield doc
107
107
 
108
108
 
109
+ def _fts_id_filter(item_id: str) -> str | None:
110
+ """The FTS5 query that finds an id's rows, or None if it has no tokens."""
111
+ if not any(c.isalnum() for c in item_id):
112
+ return None
113
+ return 'item_id:"' + item_id.replace('"', '""') + '"'
114
+
115
+
116
+ async def _delete_fts_rows(conn, collection: str, item_ids: list[str]) -> None:
117
+ """Delete the FTS rows of these ids.
118
+
119
+ FTS5 cannot use an index for `item_id = ?`, so that filter alone scans the whole
120
+ table for every id. MATCH finds the candidates through the index, and the
121
+ equality then drops other ids that tokenize alike. An id with no tokens cannot
122
+ be found by MATCH and takes the scan.
123
+ """
124
+ matched, scanned = [], []
125
+ for item_id in item_ids:
126
+ query = _fts_id_filter(item_id)
127
+ if query is None:
128
+ scanned.append((collection, item_id))
129
+ else:
130
+ matched.append((query, collection, item_id))
131
+ await conn.executemany(
132
+ """
133
+ DELETE FROM __beaver_fts_index__
134
+ WHERE __beaver_fts_index__ MATCH ? AND collection = ? AND item_id = ?
135
+ """,
136
+ matched,
137
+ )
138
+ await conn.executemany(
139
+ "DELETE FROM __beaver_fts_index__ WHERE collection = ? AND item_id = ?",
140
+ scanned,
141
+ )
142
+
143
+
109
144
  class AsyncDocumentsBatch[T: BaseModel]:
110
145
  """Async context manager for buffered bulk document indexing.
111
146
 
@@ -144,7 +179,7 @@ class AsyncDocumentsBatch[T: BaseModel]:
144
179
  return
145
180
 
146
181
  doc_rows: list[tuple[str, str, str]] = []
147
- fts_delete_ids: list[tuple[str, str]] = []
182
+ fts_delete_ids: list[str] = []
148
183
  fts_rows: list[tuple[str, str, str, str]] = []
149
184
  trigram_delete_ids: list[tuple[str, str]] = []
150
185
  trigram_rows: list[tuple[str, str, str]] = []
@@ -158,7 +193,7 @@ class AsyncDocumentsBatch[T: BaseModel]:
158
193
 
159
194
  # Always clear stale FTS / trigram rows for this id (mirrors the
160
195
  # non-batched path so re-indexing the same id is consistent).
161
- fts_delete_ids.append((self._manager._name, doc.id))
196
+ fts_delete_ids.append(doc.id)
162
197
  trigram_delete_ids.append((self._manager._name, doc.id))
163
198
 
164
199
  flat = list(_flatten_document(doc.body))
@@ -186,10 +221,7 @@ class AsyncDocumentsBatch[T: BaseModel]:
186
221
  """,
187
222
  doc_rows,
188
223
  )
189
- await conn.executemany(
190
- "DELETE FROM __beaver_fts_index__ WHERE collection = ? AND item_id = ?",
191
- fts_delete_ids,
192
- )
224
+ await _delete_fts_rows(conn, self._manager._name, fts_delete_ids)
193
225
  if fts_rows:
194
226
  await conn.executemany(
195
227
  """
@@ -270,10 +302,7 @@ class AsyncBeaverDocuments[T: BaseModel](AsyncBeaverBase[T]):
270
302
  )
271
303
 
272
304
  # 2. FTS Update (Flatten -> Delete Old -> Insert New)
273
- await self.connection.execute(
274
- "DELETE FROM __beaver_fts_index__ WHERE collection = ? AND item_id = ?",
275
- (self._name, doc.id),
276
- )
305
+ await _delete_fts_rows(self.connection, self._name, [doc.id])
277
306
 
278
307
  fts_rows = []
279
308
  for field_path, content in _flatten_document(doc.body):
@@ -323,7 +352,7 @@ class AsyncBeaverDocuments[T: BaseModel](AsyncBeaverBase[T]):
323
352
  [(self._name, item_id, t) for t in trigrams],
324
353
  )
325
354
 
326
- @atomic
355
+ @read_atomic
327
356
  async def get(self, id: str) -> Document[T]:
328
357
  """Retrieves a document by ID."""
329
358
  cursor = await self.connection.execute(
@@ -369,10 +398,7 @@ class AsyncBeaverDocuments[T: BaseModel](AsyncBeaverBase[T]):
369
398
  "DELETE FROM __beaver_documents__ WHERE collection = ? AND item_id = ?",
370
399
  (self._name, doc_id),
371
400
  )
372
- await self.connection.execute(
373
- "DELETE FROM __beaver_fts_index__ WHERE collection = ? AND item_id = ?",
374
- (self._name, doc_id),
375
- )
401
+ await _delete_fts_rows(self.connection, self._name, [doc_id])
376
402
  await self.connection.execute(
377
403
  "DELETE FROM __beaver_trigrams__ WHERE collection = ? AND item_id = ?",
378
404
  (self._name, doc_id),
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
8
8
  from pydantic import BaseModel
9
9
 
10
10
  from .api import expose, local_only
11
- from .manager import AsyncBeaverBase, atomic, emits
11
+ from .manager import AsyncBeaverBase, atomic, read_atomic, emits
12
12
  from .interfaces import IAsyncBeaverList
13
13
  from ._fracdex import key_between
14
14
 
@@ -201,7 +201,7 @@ class AsyncBeaverList[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverList[T]):
201
201
  cli_name="get",
202
202
  cli_help="Retrieve an item at an index.",
203
203
  )
204
- @atomic
204
+ @read_atomic
205
205
  async def get(self, index: Union[int, slice]) -> T | list[T]:
206
206
  """
207
207
  Retrieves an item or slice from the list.
@@ -102,7 +102,7 @@ class AsyncBeaverLock(IAsyncBeaverLock):
102
102
  """
103
103
  SELECT waiter_id FROM __beaver_lock_waiters__
104
104
  WHERE lock_name = ?
105
- ORDER BY requested_at ASC, rowid ASC
105
+ ORDER BY rowid ASC
106
106
  LIMIT 1
107
107
  """,
108
108
  (self._lock_name,),
@@ -59,6 +59,7 @@ class AsyncBeaverBase[T: BaseModel]:
59
59
  internal_lock_name,
60
60
  timeout=5.0, # Short timeout for internal operations
61
61
  lock_ttl=5.0, # Short TTL to clear crashes
62
+ poll_interval=0.005, # Held for milliseconds, so wait about as long
62
63
  )
63
64
 
64
65
  @property
@@ -173,6 +174,23 @@ def atomic(func):
173
174
  return wrapper
174
175
 
175
176
 
177
+ def read_atomic(func):
178
+ """
179
+ A decorator to run a read in a database transaction.
180
+
181
+ The transaction keeps it from seeing another task's uncommitted writes on the
182
+ shared connection. It takes no manager lock: that lock writes a row to acquire
183
+ and another to release, which would make every read a write.
184
+ """
185
+
186
+ @functools.wraps(func)
187
+ async def wrapper(self: AsyncBeaverBase, *args, **kwargs):
188
+ async with self._db.transaction():
189
+ return await func(self, *args, **kwargs)
190
+
191
+ return wrapper
192
+
193
+
176
194
  def emits(event: str | None = None, payload: Callable | None = None):
177
195
  """
178
196
  A decorator to emit an event after a manager method completes.
@@ -9,7 +9,7 @@ from typing import (
9
9
  from pydantic import BaseModel
10
10
 
11
11
  from .api import expose, local_only
12
- from .manager import AsyncBeaverBase, atomic, emits
12
+ from .manager import AsyncBeaverBase, atomic, read_atomic, emits
13
13
  from .interfaces import QueueItem, IAsyncBeaverQueue
14
14
 
15
15
 
@@ -76,7 +76,7 @@ class AsyncBeaverQueue[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverQueue[T]):
76
76
  @expose(
77
77
  path="/peek", method="GET", cli_name="peek", cli_help="Peek at the next item."
78
78
  )
79
- @atomic
79
+ @read_atomic
80
80
  async def peek(self) -> QueueItem[T] | None:
81
81
  """
82
82
  Retrieves the first item of the queue without removing it.
@@ -9,7 +9,7 @@ from typing import (
9
9
 
10
10
  from pydantic import BaseModel
11
11
 
12
- from .manager import AsyncBeaverBase, atomic, emits
12
+ from .manager import AsyncBeaverBase, atomic, read_atomic, emits
13
13
  from .interfaces import IAsyncBeaverVectors, VectorItem
14
14
  from .queries import Filter
15
15
 
@@ -169,7 +169,7 @@ class AsyncBeaverVectors[T: BaseModel](AsyncBeaverBase[T], IAsyncBeaverVectors[T
169
169
  (self._name, bucket_id, id),
170
170
  )
171
171
 
172
- @atomic
172
+ @read_atomic
173
173
  async def get(self, id: str) -> VectorItem[T]:
174
174
  cursor = await self.connection.execute(
175
175
  "SELECT vector, metadata FROM __beaver_vectors__ WHERE collection = ? AND item_id = ?",