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.
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/ci.yml +3 -0
- beaver_db-2.4.2/.github/workflows/pages.yml +62 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.gitignore +1 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/CHANGELOG.md +47 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/PKG-INFO +6 -13
- {beaver_db-2.4.0 → beaver_db-2.4.2}/README.md +4 -11
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/__init__.py +1 -1
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/blobs.py +3 -3
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/core.py +73 -20
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/dicts.py +15 -10
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/docs.py +42 -16
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/lists.py +2 -2
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/locks.py +1 -1
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/manager.py +18 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/queues.py +2 -2
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/vectors.py +2 -2
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/_quarto.yml +12 -5
- beaver_db-2.4.2/docs/beaver.scss +29 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-concurrency.md +4 -21
- beaver_db-2.4.2/docs/img/beaver.webp +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/makefile +11 -1
- {beaver_db-2.4.0 → beaver_db-2.4.2}/pyproject.toml +1 -1
- beaver_db-2.4.2/site/build.py +76 -0
- beaver_db-2.4.2/site/check_snippets.py +61 -0
- beaver_db-2.4.2/site/fonts/OFL-inter.txt +93 -0
- beaver_db-2.4.2/site/fonts/OFL-jetbrainsmono.txt +93 -0
- beaver_db-2.4.2/site/fonts/OFL-nunito.txt +93 -0
- beaver_db-2.4.2/site/fonts/inter-400.woff2 +0 -0
- beaver_db-2.4.2/site/fonts/inter-500.woff2 +0 -0
- beaver_db-2.4.2/site/fonts/inter-600.woff2 +0 -0
- beaver_db-2.4.2/site/fonts/jbmono-400.woff2 +0 -0
- beaver_db-2.4.2/site/fonts/jbmono-500.woff2 +0 -0
- beaver_db-2.4.2/site/fonts/nunito-800.woff2 +0 -0
- beaver_db-2.4.2/site/img/beaver-180.png +0 -0
- beaver_db-2.4.2/site/img/beaver-32.png +0 -0
- beaver_db-2.4.2/site/img/beaver-96.webp +0 -0
- beaver_db-2.4.2/site/img/logo-400.webp +0 -0
- beaver_db-2.4.2/site/index.html +209 -0
- beaver_db-2.4.2/site/redirects.txt +24 -0
- beaver_db-2.4.2/site/site.css +148 -0
- beaver_db-2.4.2/site/site.js +69 -0
- beaver_db-2.4.2/tests/unit/test_release_2_4_2.py +122 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_transactions.py +67 -6
- {beaver_db-2.4.0 → beaver_db-2.4.2}/uv.lock +1 -1
- beaver_db-2.4.0/docs/index.html +0 -620
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.dockerignore +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/release.yaml +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.github/workflows/tests.yaml +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.python-version +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/.vscode/settings.json +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/AGENTS.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/LICENSE +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/STATUS.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/_fracdex.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/api.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/bridge.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cache.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/channels.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/__init__.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/discovery.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/cli/main.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/client.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/errors.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/events.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/graphs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/indexing.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/interfaces.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/logs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/migrate.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/queries.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/security.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/server.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/beaver/sketches.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/design.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/dockerfile +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/.gitignore +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-architecture.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-contributing.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/dev-search.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-blobs.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-channels.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-collections.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-deployment.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-dicts.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-lists.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-locks.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-logs.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-queues.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide-sketches.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/guide.md +0 -0
- /beaver_db-2.4.0/docs/intro.md → /beaver_db-2.4.2/docs/index.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/logo.png +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/migration-1x-to-2x.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/migration-rc3-to-rc4-lists.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/quickstart.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/superpowers/plans/2026-05-24-list-fracdex-ordering.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/docs/superpowers/specs/2026-05-24-list-fracdex-ordering-design.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/async_pubsub.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/blobs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/cache.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/fts.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/fuzzy.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/general_test.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/graph.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/kvstore.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/list.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/locks.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/logs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/pqueue.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/producer_consumer.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/publisher.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/pubsub.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/rerank.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/stress_vectors.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/subscriber.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/textual_chat.css +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/textual_chat.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/type_hints.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/examples/vector.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/15-enhance-cli-with-admin-commands-shell-piping-and-interactivity.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/18-enhanced-dump-and-load-for-etl.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/19-add-comprehensive-unit-integration-and-concurrency-test-suite.md +0 -0
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/27-batched-operations-api-batched.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/28-add-hnsw-vector-strategy-beaver-dbhnsw.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/36-implement-sid-consumers-dynamic-cli-universal-client-server.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/37-v2-release-phase-0-audit-and-stabilize.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/38-v2-release-phase-1-storage-engine.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/39-v2-release-phase-2-wrappers.md +0 -0
- {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
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/42-uniform-field-indexing-across-collections.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/43-singleton-silently-ignores-differing-kwargs-on-a-cache-hit.md +0 -0
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
- {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
- {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
- {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
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/16-add-clear-method-for-all-data-structures.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/17-replace-atomic-operations-with-custom-beaver-lock.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/2-comprehensive-async-wrappers.md +0 -0
- {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
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/23-global-event-listeners-api.md +0 -0
- {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
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/30-add-probabilistic-sketches-approximate-sets-counters.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/32-secure-dictionaries-encryption-and-hashing.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/34-composable-graph-algorithms-query-builder-graphrag.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/35-refactor-core-to-shared-interface-definition-sid-architecture.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/issues/closed/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/logo.png +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/plans/2026-08-01-field-indexing-slices-1-2.md +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/pytest.ini +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/benchmark_lsh.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/concurrency/conftest.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/concurrency/test_concurrency.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/conftest.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/__init__.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/test_server_subprocess.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/integration/test_sync.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/perftest.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/test_docs_search_filters.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_api_expose.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_batched.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_blobs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_bridge.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_channels.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_cli.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_local_only.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_lists.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_logs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_client_roundtrip_queues.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_connect.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_db_version.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_dicts.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_dicts_decorated.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_docs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_errors.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_events.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_field_indexing.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_fracdex.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_graphs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists_decorated.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lists_fracdex.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_locks.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_logs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_logs_decorated.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_lsh.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_manager_events.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_migrate.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_queues.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_queues_decorated.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_security.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_auth.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_errors.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_server_routes.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_sketchs.py +0 -0
- {beaver_db-2.4.0 → beaver_db-2.4.2}/tests/unit/test_vectors.py +0 -0
|
@@ -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
|
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: beaver-db
|
|
3
|
-
Version: 2.4.
|
|
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
|
|
117
|
-
* [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html
|
|
118
|
-
* [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html
|
|
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/
|
|
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
|
|
90
|
-
* [**Full-Text & Fuzzy Search**](https://syalia.com/beaver/guide-collections.html
|
|
91
|
-
* [**Knowledge Graph**](https://syalia.com/beaver/guide-collections.html
|
|
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/
|
|
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
|
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
"""Undo a BEGIN that
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
229
|
-
except
|
|
230
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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[
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
@
|
|
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.
|
|
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
|
-
@
|
|
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.
|
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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 = ?",
|