beaver-db 2.2.0__tar.gz → 2.4.0__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 (182) hide show
  1. beaver_db-2.4.0/AGENTS.md +166 -0
  2. {beaver_db-2.2.0 → beaver_db-2.4.0}/CHANGELOG.md +51 -0
  3. {beaver_db-2.2.0 → beaver_db-2.4.0}/PKG-INFO +1 -1
  4. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/__init__.py +3 -1
  5. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/cli/discovery.py +1 -1
  6. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/core.py +191 -12
  7. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/docs.py +6 -5
  8. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/errors.py +6 -1
  9. beaver_db-2.4.0/beaver/indexing.py +260 -0
  10. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/logs.py +156 -3
  11. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/manager.py +8 -1
  12. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/dev-concurrency.md +28 -0
  13. beaver_db-2.4.0/issues/42-uniform-field-indexing-across-collections.md +410 -0
  14. beaver_db-2.4.0/issues/43-singleton-silently-ignores-differing-kwargs-on-a-cache-hit.md +100 -0
  15. beaver_db-2.4.0/issues/44-filters-have-no-cli-or-wire-encoding-so-explain-cannot-be-exposed.md +100 -0
  16. beaver_db-2.4.0/plans/2026-08-01-field-indexing-slices-1-2.md +1654 -0
  17. {beaver_db-2.2.0 → beaver_db-2.4.0}/pyproject.toml +1 -1
  18. beaver_db-2.4.0/tests/unit/test_field_indexing.py +463 -0
  19. beaver_db-2.4.0/tests/unit/test_transactions.py +299 -0
  20. {beaver_db-2.2.0 → beaver_db-2.4.0}/uv.lock +1 -1
  21. {beaver_db-2.2.0 → beaver_db-2.4.0}/.dockerignore +0 -0
  22. {beaver_db-2.2.0 → beaver_db-2.4.0}/.github/workflows/ci.yml +0 -0
  23. {beaver_db-2.2.0 → beaver_db-2.4.0}/.github/workflows/release.yaml +0 -0
  24. {beaver_db-2.2.0 → beaver_db-2.4.0}/.github/workflows/tests.yaml +0 -0
  25. {beaver_db-2.2.0 → beaver_db-2.4.0}/.gitignore +0 -0
  26. {beaver_db-2.2.0 → beaver_db-2.4.0}/.python-version +0 -0
  27. {beaver_db-2.2.0 → beaver_db-2.4.0}/.vscode/settings.json +0 -0
  28. {beaver_db-2.2.0 → beaver_db-2.4.0}/LICENSE +0 -0
  29. {beaver_db-2.2.0 → beaver_db-2.4.0}/README.md +0 -0
  30. {beaver_db-2.2.0 → beaver_db-2.4.0}/STATUS.md +0 -0
  31. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/_fracdex.py +0 -0
  32. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/api.py +0 -0
  33. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/blobs.py +0 -0
  34. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/bridge.py +0 -0
  35. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/cache.py +0 -0
  36. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/channels.py +0 -0
  37. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/cli/__init__.py +0 -0
  38. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/cli/main.py +0 -0
  39. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/client.py +0 -0
  40. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/dicts.py +0 -0
  41. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/events.py +0 -0
  42. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/graphs.py +0 -0
  43. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/interfaces.py +0 -0
  44. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/lists.py +0 -0
  45. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/locks.py +0 -0
  46. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/migrate.py +0 -0
  47. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/queries.py +0 -0
  48. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/queues.py +0 -0
  49. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/security.py +0 -0
  50. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/server.py +0 -0
  51. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/sketches.py +0 -0
  52. {beaver_db-2.2.0 → beaver_db-2.4.0}/beaver/vectors.py +0 -0
  53. {beaver_db-2.2.0 → beaver_db-2.4.0}/design.md +0 -0
  54. {beaver_db-2.2.0 → beaver_db-2.4.0}/dockerfile +0 -0
  55. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/.gitignore +0 -0
  56. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/_quarto.yml +0 -0
  57. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/dev-architecture.md +0 -0
  58. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/dev-contributing.md +0 -0
  59. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/dev-search.md +0 -0
  60. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-blobs.md +0 -0
  61. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-channels.md +0 -0
  62. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-collections.md +0 -0
  63. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-deployment.md +0 -0
  64. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-dicts.md +0 -0
  65. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-lists.md +0 -0
  66. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-locks.md +0 -0
  67. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-logs.md +0 -0
  68. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-queues.md +0 -0
  69. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide-sketches.md +0 -0
  70. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/guide.md +0 -0
  71. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/index.html +0 -0
  72. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/intro.md +0 -0
  73. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/logo.png +0 -0
  74. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/migration-1x-to-2x.md +0 -0
  75. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/migration-rc3-to-rc4-lists.md +0 -0
  76. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/quickstart.md +0 -0
  77. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/superpowers/plans/2026-05-24-list-fracdex-ordering.md +0 -0
  78. {beaver_db-2.2.0 → beaver_db-2.4.0}/docs/superpowers/specs/2026-05-24-list-fracdex-ordering-design.md +0 -0
  79. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/async_pubsub.py +0 -0
  80. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/blobs.py +0 -0
  81. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/cache.py +0 -0
  82. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/fts.py +0 -0
  83. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/fuzzy.py +0 -0
  84. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/general_test.py +0 -0
  85. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/graph.py +0 -0
  86. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/kvstore.py +0 -0
  87. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/list.py +0 -0
  88. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/locks.py +0 -0
  89. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/logs.py +0 -0
  90. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/pqueue.py +0 -0
  91. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/producer_consumer.py +0 -0
  92. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/publisher.py +0 -0
  93. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/pubsub.py +0 -0
  94. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/rerank.py +0 -0
  95. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/stress_vectors.py +0 -0
  96. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/subscriber.py +0 -0
  97. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/textual_chat.css +0 -0
  98. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/textual_chat.py +0 -0
  99. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/type_hints.py +0 -0
  100. {beaver_db-2.2.0 → beaver_db-2.4.0}/examples/vector.py +0 -0
  101. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/15-enhance-cli-with-admin-commands-shell-piping-and-interactivity.md +0 -0
  102. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/18-enhanced-dump-and-load-for-etl.md +0 -0
  103. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/19-add-comprehensive-unit-integration-and-concurrency-test-suite.md +0 -0
  104. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/24-refactor-vectorindex-to-be-a-swappable-strategy-and-add-persistent-lsh-implementation.md +0 -0
  105. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/27-batched-operations-api-batched.md +0 -0
  106. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/28-add-hnsw-vector-strategy-beaver-dbhnsw.md +0 -0
  107. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/36-implement-sid-consumers-dynamic-cli-universal-client-server.md +0 -0
  108. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/37-v2-release-phase-0-audit-and-stabilize.md +0 -0
  109. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/38-v2-release-phase-1-storage-engine.md +0 -0
  110. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/39-v2-release-phase-2-wrappers.md +0 -0
  111. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/40-batched-reentry-deadlock-under-sync-portal-on-large-sequential-writes.md +0 -0
  112. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/41-2x-silently-opens-a-1x-database-and-goes-split-brain.md +0 -0
  113. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
  114. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/10-expose-dblock-functionality-on-all-high-level-data-managers.md +0 -0
  115. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/12-add-dump-method-for-json-export-to-all-data-managers.md +0 -0
  116. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/13-adopt-pydantic-deprecate-beavermodel-and-refactor-document-to-be-generic.md +0 -0
  117. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/14-deprecate-fire-based-cli-and-build-a-feature-rich-typer-and-rich-cli.md +0 -0
  118. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/16-add-clear-method-for-all-data-structures.md +0 -0
  119. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/17-replace-atomic-operations-with-custom-beaver-lock.md +0 -0
  120. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/2-comprehensive-async-wrappers.md +0 -0
  121. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/21-implement-dbcache-property-with-wal-invalidation-dummy-fallback-and-performance-stats.md +0 -0
  122. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/22-add-context-manager-support-and-robust-process-wide-cleanup-to-beaverdb.md +0 -0
  123. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/23-global-event-listeners-api.md +0 -0
  124. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/25-refactor-core-to-async-first-architecture-with-sync-bridge.md +0 -0
  125. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/30-add-probabilistic-sketches-approximate-sets-counters.md +0 -0
  126. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/32-secure-dictionaries-encryption-and-hashing.md +0 -0
  127. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/34-composable-graph-algorithms-query-builder-graphrag.md +0 -0
  128. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/35-refactor-core-to-shared-interface-definition-sid-architecture.md +0 -0
  129. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
  130. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
  131. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
  132. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
  133. {beaver_db-2.2.0 → beaver_db-2.4.0}/issues/closed/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
  134. {beaver_db-2.2.0 → beaver_db-2.4.0}/logo.png +0 -0
  135. {beaver_db-2.2.0 → beaver_db-2.4.0}/makefile +0 -0
  136. {beaver_db-2.2.0 → beaver_db-2.4.0}/pytest.ini +0 -0
  137. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/benchmark_lsh.py +0 -0
  138. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/concurrency/conftest.py +0 -0
  139. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/concurrency/test_concurrency.py +0 -0
  140. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/conftest.py +0 -0
  141. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/integration/__init__.py +0 -0
  142. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/integration/test_server_subprocess.py +0 -0
  143. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/integration/test_sync.py +0 -0
  144. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/perftest.py +0 -0
  145. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/test_docs_search_filters.py +0 -0
  146. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_api_expose.py +0 -0
  147. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_batched.py +0 -0
  148. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_blobs.py +0 -0
  149. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_bridge.py +0 -0
  150. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_channels.py +0 -0
  151. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_cli.py +0 -0
  152. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_client_local_only.py +0 -0
  153. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_client_roundtrip.py +0 -0
  154. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_client_roundtrip_lists.py +0 -0
  155. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_client_roundtrip_logs.py +0 -0
  156. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_client_roundtrip_queues.py +0 -0
  157. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_connect.py +0 -0
  158. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_db_version.py +0 -0
  159. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_dicts.py +0 -0
  160. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_dicts_decorated.py +0 -0
  161. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_docs.py +0 -0
  162. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_errors.py +0 -0
  163. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_events.py +0 -0
  164. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_fracdex.py +0 -0
  165. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_graphs.py +0 -0
  166. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_lists.py +0 -0
  167. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_lists_decorated.py +0 -0
  168. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_lists_fracdex.py +0 -0
  169. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_locks.py +0 -0
  170. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_logs.py +0 -0
  171. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_logs_decorated.py +0 -0
  172. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_lsh.py +0 -0
  173. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_manager_events.py +0 -0
  174. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_migrate.py +0 -0
  175. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_queues.py +0 -0
  176. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_queues_decorated.py +0 -0
  177. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_security.py +0 -0
  178. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_server_auth.py +0 -0
  179. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_server_errors.py +0 -0
  180. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_server_routes.py +0 -0
  181. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_sketchs.py +0 -0
  182. {beaver_db-2.2.0 → beaver_db-2.4.0}/tests/unit/test_vectors.py +0 -0
@@ -0,0 +1,166 @@
1
+ # AGENTS.md — beaver
2
+
3
+ You're an AI agent picking up **beaver** (`beaver-db` on PyPI). This file is the door.
4
+
5
+ ## What it is
6
+
7
+ A **local-first, embedded, multi-modal database for Python**, built on a single
8
+ SQLite file. One file, many modalities: dicts, lists, queues, logs, documents
9
+ (FTS + fuzzy), vectors (LSH), graphs, blobs, locks, channels and sketches — all
10
+ in the same `.db`, all through one facade.
11
+
12
+ It is a **public library** (`syalia-srl/beaver`, MIT) with real downstream users,
13
+ not an internal utility. Its biggest consumer is AInBox (`repos/ainbox`), where
14
+ warden, magpie, superbot, peacock, help and pipelines all persist through it.
15
+
16
+ Read [`design.md`](design.md) first — the living design document, with the vision
17
+ and the guiding principles. [`STATUS.md`](STATUS.md) is the running inventory of
18
+ what is real versus missing in the v2 cycle.
19
+
20
+ ## The shape of the codebase
21
+
22
+ beaver/core.py the engine: AsyncBeaverDB + the BeaverDB sync facade,
23
+ _create_all_tables, _check_version, every factory method
24
+ beaver/interfaces.py IBeaver* protocols — the surface the SID consumers target
25
+ beaver/api.py @expose / @local_only — how a method reaches CLI + server + client
26
+ beaver/queries.py the q() filter DSL: Query.__getattr__ builds dotted paths → Filter
27
+ beaver/<modality>.py dicts, lists, queues, logs, docs, vectors, graphs, blobs,
28
+ locks, channels, sketches — one module each
29
+ beaver/migrate.py 1.x → 2.x migration
30
+ beaver/cli/ the typer CLI (entry point `beaver`)
31
+ docs/ Quarto site: guide-*.md (users), dev-*.md (contributors)
32
+ issues/ numbered issue/spec files, synced with GitHub
33
+ tests/{unit,integration,concurrency}
34
+
35
+ **Architecture in one paragraph.** `AsyncBeaverDB` is the async core (aiosqlite,
36
+ one connection, WAL). `BeaverDB` is the *synchronous facade*: it spins a reactor
37
+ thread running the async loop and bridges every call to it through
38
+ `BeaverBridge` (the Portal pattern). "Synchronous facade, asynchronous engine" —
39
+ so write the real logic in the async class, and the sync surface follows. See
40
+ `docs/dev-architecture.md` and `docs/dev-concurrency.md`.
41
+
42
+ ## Conventions
43
+
44
+ - **Every table is dunder-named** — `__beaver_logs__`, `__beaver_documents__`,
45
+ … (`core.py:63`). The single-prefix form (`beaver_logs`) is *1.x* and its
46
+ presence means a legacy file; see issue #41.
47
+ - **The `.db` stays a valid, standard SQLite file.** Anyone must be able to open
48
+ it with `sqlite3`. That is a promise from `design.md`, not a preference.
49
+ - **Schema changes are additive.** `_create_all_tables()` runs on *every*
50
+ `connect()` with `CREATE TABLE IF NOT EXISTS`, so a new table simply appears on
51
+ an existing database — no migration. Adding a side table is the house pattern
52
+ (see `__beaver_fts_index__`, `__beaver_trigrams__`, `__beaver_lsh_index__`).
53
+ - **`BEAVER_DB_VERSION` (`core.py:42`) is at 1 and moving it is a breaking
54
+ change.** `_check_version` raises `BeaverIncompatibleSchemaError` when a file's
55
+ `user_version` exceeds what the running beaver understands (`core.py:372`), so
56
+ a bump means databases written by the new release **fail to open on the old
57
+ one**. Prefer a design that fits additively; bump only when you have decided to
58
+ break file compatibility on purpose.
59
+ - **New capability on a manager? Decorate it.** `@expose(path=…, method=…,
60
+ cli_name=…, cli_help=…)` makes it reachable from the CLI, the REST server and
61
+ the remote client at once; `@local_only("reason")` marks what cannot cross the
62
+ wire (infinite streams, file-level operations). A method with neither is
63
+ local-only *by accident* rather than by decision.
64
+ - **Minimal dependencies is a product constraint.** Core is `aiosqlite`, `numpy`,
65
+ `pydantic`, `rich`, `typer`. Heavy things go behind extras (`remote`,
66
+ `security`). In particular, **vector indexing stays pure-numpy** — no
67
+ `hnswlib`, no `faiss`, no compiled deps. The LSH strategy exists precisely so
68
+ scaling did not require one (issue #24); HNSW (#28) stays deferred until a
69
+ pure-numpy ANN beats LSH at >100k vectors.
70
+ - **Silence is the bug.** The lesson of issue #41 — beaver opened a 1.x file,
71
+ reported everything empty, and wrote a parallel schema beside the untouched
72
+ data. Nothing errored; that was the problem. When state is partial, stale or
73
+ unmigrated, **say so loudly or degrade to the correct-but-slower path** — never
74
+ return a plausible subset.
75
+ - Ships to `main`. English for code, docs, comments and commit messages.
76
+
77
+ ## Working here
78
+
79
+ make sync uv sync --extra security --extra remote
80
+ uv run black . format (see the warning below)
81
+ make format-check black --check — test-unit depends on this
82
+ make type-check mypy
83
+ make test-unit pytest tests/unit --cov=beaver ← the default target
84
+ make test-all every suite, including concurrency
85
+ make ci format-check + test-unit
86
+
87
+ `pytest.ini` sets `asyncio_mode = auto` (no `@pytest.mark.asyncio` needed) and a
88
+ **30 s timeout** — a test that hangs on a lock fails rather than wedging the run.
89
+ Markers: `unit`, `integration`, `concurrency`.
90
+
91
+ > ⚠️ **`make format` and `make bugfix` commit for you**, and they do it with
92
+ > `git commit -am` / `git commit -a`, which sweeps up **every dirty file in the
93
+ > tree** — including work from a concurrent session that has nothing to do with
94
+ > yours. Use **`uv run black .`** and commit the paths you touched yourself.
95
+ > `make format-check` is safe and is what `test-unit` actually gates on.
96
+
97
+ **Releases:** `NEW_VERSION=x.y.z make release` — runs `test-all`, bumps the
98
+ version in `pyproject.toml` *and* `beaver/__init__.py`, commits, tags, pushes and
99
+ opens the GitHub release. Don't hand-edit those two version strings.
100
+
101
+ The GitHub release is what actually ships it: `.github/workflows/release.yaml`
102
+ fires on `release: created`, re-runs the suite, publishes to PyPI with
103
+ `secrets.PYPI_TOKEN`, then pushes the image to GHCR. A tag alone publishes
104
+ nothing.
105
+
106
+ **Verify against an install, not against the tag — and not against the JSON API
107
+ either.** Two things bite here, both observed releasing 2.3.0 on 2026-08-01:
108
+
109
+ - `https://pypi.org/pypi/<pkg>/<ver>/json` returned **200** while `uv pip install`
110
+ still could not resolve the version. The JSON API and the simple index that
111
+ installers actually read propagate separately, so a 200 there is a proxy
112
+ signal, not proof the package is installable.
113
+ - `uv` caches the index, and **`--refresh` does not clear it** — the install kept
114
+ failing with *"there is no version of beaver-db==2.3.0"* long after the simple
115
+ index listed both artifacts. `--no-cache` is what works.
116
+
117
+ So the real check is a scratch venv that exercises the feature:
118
+
119
+ ```bash
120
+ uv venv /tmp/verify && VIRTUAL_ENV=/tmp/verify uv pip install --no-cache "beaver-db==X.Y.Z"
121
+ VIRTUAL_ENV=/tmp/verify uv run --no-project python -c "…exercise the new API…"
122
+ ```
123
+
124
+ **Issues are files.** `issues/NN-slug.md` with `number / title / state / labels`
125
+ frontmatter, round-tripped to GitHub by `make issues` (needs the `gh md-issues`
126
+ extension). A substantial feature gets an issue file written *first* — see
127
+ [`issues/24-…`](issues/) (LSH strategy) and
128
+ [`issues/42-uniform-field-indexing-across-collections.md`](issues/42-uniform-field-indexing-across-collections.md)
129
+ (field indexing) for the shape: Summary → the problem with real line references →
130
+ Design → costs → delivery order → testing.
131
+
132
+ ## Gotchas paid for in advance
133
+
134
+ - **FTS5 rejects punctuation in the query string** — tokenize before you hand a
135
+ user's words to `.search()` / `.fts()`. Measured on 2.2.0 against an indexed
136
+ document, three different `OperationalError`s:
137
+
138
+ | query | error |
139
+ |---|---|
140
+ | `thing` | — (1 hit) |
141
+ | `what's this?` | `fts5: syntax error near "'"` |
142
+ | `this?` | `fts5: syntax error near "?"` |
143
+ | `a-b` | `no such column: b` |
144
+
145
+ The hyphen case is the trap: FTS5 reads `a-b` as a **column filter**, so you
146
+ get a "no such column" error that says nothing about punctuation and sends you
147
+ hunting for a schema bug that isn't there.
148
+ - **There is no reranking.** `docs` gives you FTS rank and trigram counts;
149
+ combining lexical and vector results is the caller's job (roll your own RRF).
150
+ - **Embeddings are not part of `docs`.** A `docs()` collection and a `vectors()`
151
+ collection are separate things you keep in sync yourself; `docs` never embeds.
152
+ - **`docs().where()` is a full scan today** — it compiles to `json_extract` per
153
+ row (`docs.py:472`), which is fast but not an index. Issue #42 is the fix.
154
+ - **`__beaver_priority_queues__` has no declared PRIMARY KEY.** Its implicit
155
+ `rowid` is the only stable per-item identity. Any code assuming every table
156
+ declares a PK is wrong.
157
+ - **`log()` resolves timestamp collisions by micro-incrementing and retrying**
158
+ (`logs.py:96`). Under many concurrent writers that loop can spin — use
159
+ `batched()`, which enforces monotonicity up front.
160
+
161
+ ## Where the planning lives
162
+
163
+ Design docs and the v2 release plan live in Alex's workspace vault
164
+ (`vault/Atlas/Architecture/2026-05-15-beaver-v2-release-plan.md`), not in this
165
+ repo — this repo carries `design.md`, `STATUS.md` and `issues/`. If you need the
166
+ wider roadmap and cannot see the vault, ask rather than guess.
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to beaver-db will be recorded here.
4
4
 
5
+ ## 2.4.0 — 2026-08-10
6
+
7
+ ### Fixed — availability class
8
+
9
+ - **A cancelled transaction no longer deadlocks every writer.** `Transaction.
10
+ __aenter__` had two windows in which it could leave `_tx_lock` held with
11
+ `__aexit__` never running, because the `async with` body was never entered:
12
+ cancellation between acquiring the lock and returning, and `BEGIN IMMEDIATE`
13
+ raising. Both left the lock held forever; the cancellation case *also* left
14
+ SQLite's write lock held, because cancelling the wait on
15
+ `connection.execute("BEGIN IMMEDIATE")` does not un-run the statement on
16
+ aiosqlite's worker thread.
17
+
18
+ In WAL mode this is nearly invisible: readers keep working while every
19
+ writer queues behind a lock with no owner. It took down authentication on an
20
+ AInBox appliance for hours on 2026-08-10 while the process kept serving its
21
+ login page and passing its healthcheck. Starlette/uvicorn cancel a request
22
+ task whenever a client disconnects, so the trigger is ordinary.
23
+
24
+ The enter path now cleans up after itself: a failed or cancelled `BEGIN` is
25
+ rolled back (shielded, so it survives the cancellation that caused it) and
26
+ the lock is released in a `finally`.
27
+
28
+ - **Ownership is claimed only once the transaction has actually begun.**
29
+ `_tx_owner_task` was set *before* `BEGIN IMMEDIATE`, so a raising BEGIN left
30
+ it naming a transaction that never started — and the reentrancy check then
31
+ waved that task's next transaction through as a silent no-op, so a rolled
32
+ back block would persist.
33
+
34
+ - **The reentrancy check no longer matches outside a task.**
35
+ `_tx_owner_task == current_task` was true when both were `None`, which
36
+ reads a caller with no running task as already owning the transaction.
37
+
38
+ ### Added
39
+
40
+ - **`TransactionTimeoutError`** (exported from `beaver`, subclass of
41
+ `TimeoutError`). Waiting for the write lock is now bounded by
42
+ `connection_timeout` (default 30 s) instead of being unbounded. beaver
43
+ multiplexes every modality over a single connection, so contending tasks
44
+ queue on an in-process `asyncio.Lock` that SQLite's busy-timeout — which
45
+ only arbitrates between *separate* connections — never gets a say over.
46
+ An unbounded wait there is indistinguishable from a hang.
47
+
48
+ - **`db.transaction(timeout=…)`** overrides that bound per call, for callers
49
+ with their own latency budget (a healthcheck, a request handler).
50
+
51
+ - **An orphaned transaction is reclaimed, not waited on.** If the lock's owner
52
+ is a task that has already finished, the next writer rolls its transaction
53
+ back, logs a warning on the `beaver` logger and takes over. Recovery is
54
+ loud on purpose: it means something leaked.
55
+
5
56
  ## 2.2.0 — 2026-07-29
6
57
 
7
58
  ### Fixed — data-loss class (#41)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaver-db
3
- Version: 2.2.0
3
+ Version: 2.4.0
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
@@ -3,6 +3,7 @@ from .core import (
3
3
  AsyncBeaverDB,
4
4
  BeaverIncompatibleSchemaError,
5
5
  BeaverLegacySchemaError,
6
+ TransactionTimeoutError,
6
7
  )
7
8
  from .docs import Document
8
9
  from .events import Event
@@ -10,7 +11,7 @@ from .queries import q
10
11
  from .security import Secret
11
12
  from .errors import BeaverRemoteError, LocalOnlyError
12
13
 
13
- __version__ = "2.2.0"
14
+ __version__ = "2.4.0"
14
15
 
15
16
 
16
17
  def connect(source: str, *, sync: bool = True, api_key: str | None = None):
@@ -39,6 +40,7 @@ __all__ = [
39
40
  "BeaverDB",
40
41
  "BeaverIncompatibleSchemaError",
41
42
  "BeaverLegacySchemaError",
43
+ "TransactionTimeoutError",
42
44
  "Document",
43
45
  "Secret",
44
46
  "Event",
@@ -171,7 +171,7 @@ def _build_command(method_name: str, meta: EndpointMeta, manager_accessor: Calla
171
171
  _run(ctx, manager_accessor, method_name, block=block, timeout=timeout)
172
172
 
173
173
  # --- shared no-arg shapes ---
174
- elif method_name in ("count", "clear"):
174
+ elif method_name in ("count", "clear", "indexes"):
175
175
 
176
176
  def cmd(ctx: typer.Context):
177
177
  _run(ctx, manager_accessor, method_name)
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import logging
3
4
  import threading
4
5
  from typing import Any, Self, Type, AsyncContextManager
5
6
 
@@ -41,6 +42,8 @@ from .vectors import AsyncBeaverVectors
41
42
 
42
43
  BEAVER_DB_VERSION = 1
43
44
 
45
+ _log = logging.getLogger("beaver")
46
+
44
47
 
45
48
  class BeaverIncompatibleSchemaError(RuntimeError):
46
49
  """Raised when opening a database whose schema is incompatible with this
@@ -82,6 +85,19 @@ LEGACY_1X_TABLES = frozenset(
82
85
  )
83
86
 
84
87
 
88
+ class TransactionTimeoutError(TimeoutError):
89
+ """Raised when a transaction could not take the write lock in time.
90
+
91
+ beaver multiplexes every modality over a *single* connection, so tasks that
92
+ want to write queue on an in-process lock and SQLite's busy-timeout — which
93
+ only arbitrates between separate connections — never applies to them. The
94
+ bound is `connection_timeout` (or the per-call `timeout=` on
95
+ ``db.transaction()``).
96
+
97
+ Seeing this means a writer is holding the transaction open longer than the
98
+ budget: a long-running block, a stalled task, or a genuine write storm."""
99
+
100
+
85
101
  class Transaction:
86
102
  """
87
103
  A Reentrant Async Context Manager for database transactions.
@@ -91,29 +107,128 @@ class Transaction:
91
107
  at the outermost level.
92
108
  """
93
109
 
94
- def __init__(self, db: "AsyncBeaverDB"):
110
+ def __init__(self, db: "AsyncBeaverDB", *, timeout: float | None = None):
95
111
  self.db = db
96
112
  self._is_root = False
113
+ self._timeout = db._timeout if timeout is None else timeout
97
114
 
98
115
  async def __aenter__(self):
99
116
  current_task = asyncio.current_task()
100
117
 
101
118
  # 1. Check Reentrancy: Do we already own the transaction?
102
- if self.db._tx_owner_task == current_task:
119
+ # `is not None` matters: outside a task `current_task()` is None,
120
+ # and a bare `==` would read that as owning the transaction.
121
+ if current_task is not None and self.db._tx_owner_task is current_task:
103
122
  self._is_root = False
104
123
  return self
105
124
 
106
- # 2. Acquire Lock (Wait for other tasks)
107
- await self.db._tx_lock.acquire()
125
+ # 2. Acquire the write lock, bounded (see _acquire).
126
+ await self._acquire()
127
+
128
+ # 3. Start the DB transaction, THEN claim ownership.
129
+ #
130
+ # Order matters. `__aexit__` never runs when `__aenter__` raises, so
131
+ # everything between the acquire above and the return below has to
132
+ # clean up after itself — and ownership must not name a transaction
133
+ # that never began, or the reentrancy check above waves this task's
134
+ # next transaction through as a no-op.
135
+ #
136
+ # Using isolation_level=None globally means we MUST start manually.
137
+ try:
138
+ await self.db.connection.execute("BEGIN IMMEDIATE")
139
+ except BaseException:
140
+ try:
141
+ await self._rollback_quietly()
142
+ finally:
143
+ self.db._tx_lock.release()
144
+ raise
108
145
 
109
- # 3. Mark ownership and start DB transaction
110
146
  self.db._tx_owner_task = current_task
111
147
  self._is_root = True
112
-
113
- # Using isolation_level=None globally means we MUST start manually
114
- await self.db.connection.execute("BEGIN IMMEDIATE")
115
148
  return self
116
149
 
150
+ async def _acquire(self) -> None:
151
+ """Take the write lock, waiting at most `self._timeout` seconds.
152
+
153
+ The wait has to be bounded because beaver multiplexes every modality
154
+ over a *single* connection: contending tasks queue here, on an
155
+ in-process `asyncio.Lock`, and SQLite's busy-timeout — which only
156
+ arbitrates between separate connections — never gets a say. An
157
+ unbounded wait here is indistinguishable from a hang, and in WAL mode
158
+ readers keep working throughout, so the process looks alive while every
159
+ writer is stuck behind it.
160
+ """
161
+ if await self._steal_if_orphaned():
162
+ return
163
+
164
+ try:
165
+ async with asyncio.timeout(self._timeout):
166
+ await self.db._tx_lock.acquire()
167
+ except TimeoutError:
168
+ # A holder that finished without releasing may have appeared while
169
+ # we waited; take it over rather than reporting contention.
170
+ if await self._steal_if_orphaned():
171
+ return
172
+ raise TransactionTimeoutError(
173
+ f"could not take the database write lock within "
174
+ f"{self._timeout:g}s — another task is holding a transaction "
175
+ f"open on {self.db._db_path!r}"
176
+ ) from None
177
+
178
+ async def _steal_if_orphaned(self) -> bool:
179
+ """Take over a lock whose owner has already finished.
180
+
181
+ A task that is `done()` cannot still be inside its `async with` body,
182
+ so nobody is coming back to commit or release: waiting out the timeout
183
+ would only delay an error. The rollback matters as much as the lock —
184
+ the orphan's `BEGIN IMMEDIATE` is what pins the file for every *other*
185
+ connection too, and SQLite refuses to begin a transaction inside one.
186
+
187
+ Returns True when this call now holds the lock. The lock object stays
188
+ `locked()` throughout; only ownership changes hands, so tasks already
189
+ queued on it keep waiting instead of racing us for it.
190
+
191
+ An owner of None means someone is mid-BEGIN and has not claimed the
192
+ lock yet — that is live, not orphaned. Nothing else can be `done()` and
193
+ still be running its transaction body, so there is no false positive
194
+ here that would roll back somebody's in-flight work.
195
+ """
196
+ owner = self.db._tx_owner_task
197
+ if owner is None or not owner.done() or not self.db._tx_lock.locked():
198
+ return False
199
+
200
+ # Loud, per beaver's rule about partial state: reclaiming is recovery
201
+ # from a bug somewhere, not a normal event, and a silent recovery is
202
+ # how the caller never learns it has a leak.
203
+ _log.warning(
204
+ "reclaiming an orphaned transaction on %r: its owning task (%s) "
205
+ "finished without committing or rolling back",
206
+ self.db._db_path,
207
+ owner.get_name(),
208
+ )
209
+ await self._rollback_quietly()
210
+ self.db._tx_owner_task = None
211
+ return True
212
+
213
+ async def _rollback_quietly(self) -> None:
214
+ """Undo a BEGIN that may have taken effect even though its await did not.
215
+
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.
221
+
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
+ try:
228
+ await asyncio.shield(self.db.connection.rollback())
229
+ except Exception:
230
+ pass
231
+
117
232
  async def __aexit__(self, exc_type, exc, tb):
118
233
  # Only the root transaction performs the Commit/Rollback and Release
119
234
  if not self._is_root:
@@ -152,6 +267,10 @@ class AsyncBeaverDB:
152
267
  pragma_mmap_size: int = 256 * 1024 * 1024,
153
268
  ):
154
269
  self._db_path = db_path
270
+ # Bounds both ways a write can be made to wait: SQLite's busy-timeout
271
+ # (contention from another *connection*) and the transaction lock below
272
+ # (contention from another *task* on this one). Only the second one
273
+ # applies in a single-process app, which is why it must be bounded too.
155
274
  self._timeout = connection_timeout
156
275
  self._cache_timeout = cache_timeout
157
276
 
@@ -256,12 +375,18 @@ class AsyncBeaverDB:
256
375
 
257
376
  return self._connection
258
377
 
259
- def transaction(self) -> AsyncContextManager:
378
+ def transaction(self, *, timeout: float | None = None) -> AsyncContextManager:
260
379
  """
261
380
  Returns an async context manager for an atomic transaction.
262
381
  Use: async with db.transaction(): ...
382
+
383
+ `timeout` bounds the wait for the write lock, defaulting to this
384
+ database's `connection_timeout`. Pass a tighter one from a caller with
385
+ its own latency budget — a healthcheck, a request handler — so a
386
+ contended write fails fast with `TransactionTimeoutError` instead of
387
+ holding the caller for the full default.
263
388
  """
264
- return Transaction(self)
389
+ return Transaction(self, timeout=timeout)
265
390
 
266
391
  async def _tables_named(self, names) -> list[str]:
267
392
  """Return which of `names` exist as tables in the open database."""
@@ -484,6 +609,41 @@ class AsyncBeaverDB:
484
609
  "CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON __beaver_logs__ (log_name, timestamp)"
485
610
  )
486
611
 
612
+ # Field Index (issue #42) — uniform per-field indexing.
613
+ # Additive by design: new tables only, no ALTER, no version bump.
614
+ await c.execute(
615
+ """
616
+ CREATE TABLE IF NOT EXISTS __beaver_field_index__ (
617
+ kind TEXT NOT NULL,
618
+ name TEXT NOT NULL,
619
+ item_key TEXT NOT NULL,
620
+ field TEXT NOT NULL,
621
+ value TEXT,
622
+ value_num REAL,
623
+ PRIMARY KEY (kind, name, item_key, field)
624
+ )
625
+ """
626
+ )
627
+ await c.execute(
628
+ "CREATE INDEX IF NOT EXISTS idx_field_lookup "
629
+ "ON __beaver_field_index__ (kind, name, field, value)"
630
+ )
631
+ await c.execute(
632
+ "CREATE INDEX IF NOT EXISTS idx_field_lookup_num "
633
+ "ON __beaver_field_index__ (kind, name, field, value_num)"
634
+ )
635
+ await c.execute(
636
+ """
637
+ CREATE TABLE IF NOT EXISTS __beaver_field_index_manifest__ (
638
+ kind TEXT NOT NULL,
639
+ name TEXT NOT NULL,
640
+ field TEXT NOT NULL,
641
+ complete INTEGER NOT NULL DEFAULT 0,
642
+ PRIMARY KEY (kind, name, field)
643
+ )
644
+ """
645
+ )
646
+
487
647
  # Priority Queues
488
648
  await c.execute(
489
649
  """
@@ -685,9 +845,28 @@ class AsyncBeaverDB:
685
845
  return self.singleton(AsyncBeaverBlob, name, model=model)
686
846
 
687
847
  def log[T: BaseModel](
688
- self, name: str, model: type[T] | None = None
848
+ self,
849
+ name: str,
850
+ model: type[T] | None = None,
851
+ # Quoted: inside a PEP 695 generic scope the class body is visible, so
852
+ # a bare `list[str]` resolves to AsyncBeaverDB.list, not the builtin.
853
+ indexed: "list[str] | None" = None,
689
854
  ) -> AsyncBeaverLog[T]:
690
- return self.singleton(AsyncBeaverLog, name, model=model)
855
+ """Open a log.
856
+
857
+ `indexed` declares the fields whose values are maintained in the field
858
+ index. **Declare on first use:** `singleton()` keys its cache on
859
+ `(cls, name)` only, so after a bare `db.log("audit")` a later
860
+ `db.log("audit", indexed=[...])` returns the already-cached, undeclared
861
+ manager and the declaration is silently dropped.
862
+ """
863
+ return self.singleton(AsyncBeaverLog, name, model=model, indexed=indexed)
864
+
865
+ async def indexes(self) -> dict:
866
+ """Every declared field index in this database, by (kind, name)."""
867
+ from . import indexing
868
+
869
+ return await indexing.all_field_indexes(self.connection)
691
870
 
692
871
  def lock(
693
872
  self, name: str, timeout=None, lock_ttl=60.0, poll_interval=0.1
@@ -11,6 +11,7 @@ from typing import (
11
11
 
12
12
  from pydantic import BaseModel
13
13
 
14
+ from . import indexing
14
15
  from .queries import Filter
15
16
  from .manager import AsyncBeaverBase, atomic, emits
16
17
  from .interfaces import Document, ScoredDocument, IDocumentQuery
@@ -468,11 +469,11 @@ class AsyncBeaverDocuments[T: BaseModel](AsyncBeaverBase[T]):
468
469
  params.extend(q._search_fields)
469
470
 
470
471
  if q._filters:
471
- for filter in q._filters:
472
- where.append(
473
- f"json_extract(d.data, '$.{filter.path}') {filter.operator} ?"
474
- )
475
- params.append(filter.value)
472
+ clauses, filter_params = indexing.compile_scan_filters(
473
+ "data", q._filters, alias="d"
474
+ )
475
+ where.extend(clauses)
476
+ params.extend(filter_params)
476
477
 
477
478
  parts.append("WHERE " + " AND ".join(where))
478
479
 
@@ -3,7 +3,11 @@
3
3
  import pydantic
4
4
  from pydantic import BaseModel
5
5
 
6
- from .core import BeaverIncompatibleSchemaError, BeaverLegacySchemaError
6
+ from .core import (
7
+ BeaverIncompatibleSchemaError,
8
+ BeaverLegacySchemaError,
9
+ TransactionTimeoutError,
10
+ )
7
11
 
8
12
 
9
13
  class BeaverRemoteError(Exception):
@@ -26,6 +30,7 @@ _REGISTRY: dict[str, type[Exception]] = {
26
30
  "TimeoutError": TimeoutError,
27
31
  "BeaverIncompatibleSchemaError": BeaverIncompatibleSchemaError,
28
32
  "BeaverLegacySchemaError": BeaverLegacySchemaError,
33
+ "TransactionTimeoutError": TransactionTimeoutError,
29
34
  "ValidationError": pydantic.ValidationError,
30
35
  }
31
36