lodedb 1.3.1__tar.gz → 2.0.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.
- {lodedb-1.3.1 → lodedb-2.0.0}/Cargo.toml +4 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/PKG-INFO +126 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/README.md +120 -1
- lodedb-2.0.0/crates/lodedb-cloud-core/Cargo.toml +41 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/artifact_store.rs +127 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/blob_layout.rs +119 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/client_ops.rs +621 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/digest.rs +46 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/error.rs +133 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/generation_inventory.rs +502 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/lib.rs +83 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/local_artifact_store.rs +190 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/managed.rs +440 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/manifest_transfer.rs +245 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/object_artifact_store.rs +495 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/paths.rs +153 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/snapshot_identity.rs +72 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/status.rs +143 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/store_target.rs +58 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/sync_plan.rs +225 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/sync_state.rs +212 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/transfer_policy.rs +81 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/src/verify.rs +160 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/artifact_store.rs +236 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/client_ops.rs +105 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/common/mod.rs +276 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/generation_inventory.rs +413 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/managed.rs +268 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/manifest_transfer.rs +317 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/object_artifact_store.rs +285 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/restore.rs +283 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/snapshot_identity.rs +67 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/store_target.rs +35 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_ops.rs +663 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_plan.rs +273 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_state.rs +125 -0
- lodedb-2.0.0/crates/lodedb-cloud-core/tests/verify_status.rs +183 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/write_generation_fixture.rs +1 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/write_wal_fixture.rs +1 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/engine/mod.rs +1758 -291
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lib.rs +1 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/commit_manifest.rs +53 -9
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/mod.rs +60 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/multivec_store.rs +1 -20
- lodedb-2.0.0/crates/lodedb-core/src/storage/tvvf_store.rs +2088 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/util.rs +88 -5
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/wal.rs +220 -10
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/types.rs +32 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/turbovec.rs +111 -10
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/version.rs +6 -2
- lodedb-2.0.0/crates/lodedb-core/tests/block_skip_layout.rs +55 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/core_engine.rs +2025 -18
- lodedb-2.0.0/crates/lodedb-core/tests/operator_observability.rs +216 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/storage_fixtures.rs +7 -0
- lodedb-2.0.0/crates/lodedb-core/tests/turbovec_adapter.rs +191 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-gpu/src/lib.rs +4 -4
- lodedb-2.0.0/crates/lodedb-graph/Cargo.toml +28 -0
- lodedb-2.0.0/crates/lodedb-graph/src/error.rs +61 -0
- lodedb-2.0.0/crates/lodedb-graph/src/graph.rs +809 -0
- lodedb-2.0.0/crates/lodedb-graph/src/index.rs +883 -0
- lodedb-2.0.0/crates/lodedb-graph/src/lib.rs +51 -0
- lodedb-2.0.0/crates/lodedb-graph/src/model.rs +231 -0
- lodedb-2.0.0/crates/lodedb-graph/src/search.rs +278 -0
- lodedb-2.0.0/crates/lodedb-graph/src/temporal.rs +126 -0
- lodedb-2.0.0/crates/lodedb-graph/src/topology.rs +1372 -0
- lodedb-2.0.0/crates/lodedb-graph/tests/temporal_graph.rs +608 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/pyproject.toml +19 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/__init__.py +4 -2
- lodedb-2.0.0/src/lodedb/_native_core.py +77 -0
- lodedb-2.0.0/src/lodedb/cloud/__init__.py +113 -0
- lodedb-2.0.0/src/lodedb/cloud/_agents_scaffold.py +204 -0
- lodedb-2.0.0/src/lodedb/cloud/_config.py +159 -0
- lodedb-2.0.0/src/lodedb/cloud/_env_file.py +120 -0
- lodedb-2.0.0/src/lodedb/cloud/_sealing.py +60 -0
- lodedb-2.0.0/src/lodedb/cloud/cli.py +1845 -0
- lodedb-2.0.0/src/lodedb/cloud/client.py +447 -0
- lodedb-2.0.0/src/lodedb/cloud/serving.py +972 -0
- lodedb-2.0.0/src/lodedb/cloud/transfer.py +1086 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_atomic_io.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_commit_manifest.py +6 -6
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_filelock.py +4 -4
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_lexical.py +3 -3
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_predicate.py +4 -4
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/embedding_backends.py +63 -34
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/gpu_turbovec.py +3 -3
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/native_adapter.py +42 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/route_registry.py +1 -1
- lodedb-2.0.0/src/lodedb/graph/__init__.py +27 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/graph/_store.py +2 -2
- lodedb-2.0.0/src/lodedb/graph/temporal.py +362 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/__init__.py +7 -5
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/backends.py +1 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/benchmark.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/cli.py +46 -4
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/db.py +518 -63
- lodedb-2.0.0/src/lodedb/local/doctor.py +729 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/__init__.py +14 -9
- lodedb-2.0.0/src/lodedb/local/integrations/kotaemon.py +604 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/langchain.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/llama_index.py +1 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/llama_index_graph.py +3 -3
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/privategpt.py +5 -5
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/mcp_install.py +5 -5
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/mcp_server.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/detect.py +4 -4
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/report.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/runner.py +1 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/__init__.py +4 -4
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/base.py +2 -2
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/onnx_artifacts.py +3 -3
- lodedb-2.0.0/src/lodedb/local/segments.py +187 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/server.py +52 -1
- lodedb-2.0.0/third_party/turbovec/Cargo.lock +3095 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/Cargo.toml +1 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/error.rs +31 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/id_map.rs +40 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/lib.rs +64 -1
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/id_map.rs +133 -0
- lodedb-2.0.0/third_party/turbovec/turbovec-python/Cargo.toml +29 -0
- lodedb-2.0.0/third_party/turbovec/turbovec-python/src/cloud.rs +455 -0
- lodedb-2.0.0/third_party/turbovec/turbovec-python/src/graph.rs +510 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/src/lib.rs +342 -80
- lodedb-1.3.1/crates/lodedb-core/tests/turbovec_adapter.rs +0 -90
- lodedb-1.3.1/src/lodedb/_native_core.py +0 -35
- lodedb-1.3.1/src/lodedb/graph/__init__.py +0 -13
- lodedb-1.3.1/src/lodedb/local/doctor.py +0 -298
- lodedb-1.3.1/third_party/turbovec/Cargo.lock +0 -1455
- lodedb-1.3.1/third_party/turbovec/turbovec-python/Cargo.toml +0 -21
- {lodedb-1.3.1 → lodedb-2.0.0}/LICENSE +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/NOTICE +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/Cargo.toml +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/filter_planner_bench.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/turbovec_adapter_smoke.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/error.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/ast.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/doc_set.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/field_index.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/mod.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/predicate.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/resolve.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/validate.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/bm25.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/mod.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/rrf.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/tokenize.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/legacy.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/lexical_store.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/lsn.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/state_journal.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/text_store.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/tvann_store.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/tvim_delta.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/chunk.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/hash.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/mod.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/ann.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/index.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/math.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/mod.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/stable_id.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/ann_build_bench.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/filter_plan_fixtures.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/identity_fixtures.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/lexical_fixtures.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/predicate_fixtures.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-gpu/Cargo.toml +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/__main__.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/config.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/__init__.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/core.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/route_profiles.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/runtime_policy.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/state_journal_store.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/turbovec_delta_store.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/turbovec_index.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/graph/knowledge_graph.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/appender.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/checkpointer.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/collection.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/cognee.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/mem0.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/late_interaction.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/__init__.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/cli.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/plan.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/langchain_inmemory.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/llama_index_simple.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/mem0_qdrant.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/pgvector.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/presets.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/py.typed +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/Cargo.toml +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/README.md +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/build.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/examples/dump_state.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/examples/kernel_xtest.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/codebook.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/encode.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/io.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/maxsim.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/pack.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/rotation.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/search.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/codebook.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/concurrent_search.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/distortion.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/encode.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/encoded_rows.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/filtering.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/input_validation.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/io_versioning.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/kernel_correctness.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/lazy_init.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/reconstruction.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/rotation.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/state_sequences.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/swap_remove.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/tqplus_calibration.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/README.md +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/build.rs +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/__init__.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/_dedup.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/_persist.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/agno.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/haystack.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/langchain.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/llama_index.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_agno.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_dedup.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_filtering.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_haystack.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_id_map.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_index.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_langchain.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_llama_index.py +0 -0
- {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_security.py +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
[workspace]
|
|
2
2
|
members = [
|
|
3
|
+
"crates/lodedb-cloud-core",
|
|
3
4
|
"crates/lodedb-core",
|
|
4
5
|
"crates/lodedb-ffi",
|
|
5
6
|
"crates/lodedb-gpu",
|
|
7
|
+
"crates/lodedb-graph",
|
|
6
8
|
]
|
|
7
9
|
resolver = "2"
|
|
8
10
|
|
|
9
11
|
[workspace.package]
|
|
10
|
-
version = "
|
|
12
|
+
version = "2.0.0"
|
|
11
13
|
edition = "2021"
|
|
12
|
-
rust-version = "1.
|
|
14
|
+
rust-version = "1.88"
|
|
13
15
|
license = "Apache-2.0"
|
|
14
16
|
repository = "https://github.com/Egoist-Machines/LodeDB"
|
|
15
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lodedb
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -10,6 +10,9 @@ Requires-Dist: numpy>=2.0.0,<3
|
|
|
10
10
|
Requires-Dist: typer>=0.12.0
|
|
11
11
|
Requires-Dist: pyyaml>=6.0.0
|
|
12
12
|
Requires-Dist: lodedb[embeddings,torch,image,mcp,langchain,llama-index,mem0] ; extra == 'all'
|
|
13
|
+
Requires-Dist: httpx>=0.27 ; extra == 'cloud'
|
|
14
|
+
Requires-Dist: pynacl>=1.5 ; extra == 'cloud'
|
|
15
|
+
Requires-Dist: cryptography>=47 ; extra == 'cloud-sealed'
|
|
13
16
|
Requires-Dist: cognee>=1.1.0,<2 ; python_full_version < '3.15' and extra == 'cognee'
|
|
14
17
|
Requires-Dist: pytest>=8.3.0 ; extra == 'dev'
|
|
15
18
|
Requires-Dist: ruff==0.15.17 ; extra == 'dev'
|
|
@@ -25,6 +28,8 @@ Requires-Dist: mem0ai>=2.0.0 ; extra == 'mem0'
|
|
|
25
28
|
Requires-Dist: optimum[exporters]>=1.23.0,<2.0.0 ; extra == 'onnx-export'
|
|
26
29
|
Requires-Dist: sentence-transformers>=3.0.0,<5 ; extra == 'torch'
|
|
27
30
|
Provides-Extra: all
|
|
31
|
+
Provides-Extra: cloud
|
|
32
|
+
Provides-Extra: cloud-sealed
|
|
28
33
|
Provides-Extra: cognee
|
|
29
34
|
Provides-Extra: dev
|
|
30
35
|
Provides-Extra: embeddings
|
|
@@ -388,6 +393,42 @@ db.search("turbine fault", k=5) # nearest cluste
|
|
|
388
393
|
`ann_clusters` (partitions) defaults to about `sqrt(n)` and `ann_nprobe` (clusters probed per
|
|
389
394
|
query) to about `sqrt(clusters)`. ANN is a create-time choice persisted with the index and works
|
|
390
395
|
for text and bring-your-own-vector indexes alike; keep the exact default for small-to-mid corpora.
|
|
396
|
+
On reopen, `ann_nprobe` is a session-only override, so benchmark handles can change probe breadth
|
|
397
|
+
without rebuilding; the algorithm and partition count remain the persisted configuration.
|
|
398
|
+
|
|
399
|
+
For large corpora, these are useful starting ranges to measure, not recall or latency promises:
|
|
400
|
+
|
|
401
|
+
| Knob | Useful range | Trade-off |
|
|
402
|
+
| --- | --- | --- |
|
|
403
|
+
| `ann_nprobe` | 64 to 256 clusters | More probes raise recall and scan work; probing every cluster is exact. |
|
|
404
|
+
| `rescore_oversample` | 2 to 8 times `k` | More candidates improve the chance that fp32 re-ranking repairs compact-code errors, at more sidecar reads. |
|
|
405
|
+
</details>
|
|
406
|
+
|
|
407
|
+
<details>
|
|
408
|
+
<summary><b>Two-stage rescore (`rescore="original"`)</b></summary>
|
|
409
|
+
|
|
410
|
+
TurboVec's compact 4-bit scan is fast, but its quantized first-stage ranking can put close
|
|
411
|
+
neighbors in the wrong order. Enable rescore at creation to retain each input vector in a separate
|
|
412
|
+
original-precision sidecar, then re-score the first-stage candidate pool with fp32 dot products:
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
db = LodeDB(
|
|
416
|
+
path="./data",
|
|
417
|
+
ann="cluster",
|
|
418
|
+
ann_clusters=1024,
|
|
419
|
+
rescore="original",
|
|
420
|
+
rescore_dtype="float16", # default; about 2 bytes per dimension per vector
|
|
421
|
+
rescore_oversample=4,
|
|
422
|
+
)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`float16` sidecars add about 2 bytes per dimension per vector (`float32` adds 4; `int8` adds 1).
|
|
426
|
+
They let the final ordering rise above the practical 4-bit ranking ceiling while retaining the
|
|
427
|
+
compact index for candidate generation. Re-scored candidate scores are exact fp32 dots, but the
|
|
428
|
+
overall result can still be approximate when ANN or a too-small candidate pool excludes a true
|
|
429
|
+
neighbor. Rescore is a create-time capture choice: originals are not recoverable after a normal
|
|
430
|
+
store's first ingest, so enabling it later requires a rebuild. On a rescore-enabled reopen,
|
|
431
|
+
`rescore_oversample` is a session-only override; mode and dtype must match the persisted sidecar.
|
|
391
432
|
</details>
|
|
392
433
|
|
|
393
434
|
## Multimodal & bring-your-own vectors
|
|
@@ -537,6 +578,9 @@ lodedb mcp # stdio MCP server for agent memory
|
|
|
537
578
|
lodedb benchmark # local, metrics-only benchmark
|
|
538
579
|
```
|
|
539
580
|
|
|
581
|
+
Use `lodedb serve` as a local OpenAI-compatible embeddings provider; see [LodeDB as a local
|
|
582
|
+
embeddings provider](docs/integrations.md#lodedb-as-a-local-embeddings-provider).
|
|
583
|
+
|
|
540
584
|
## Use as an MCP server
|
|
541
585
|
|
|
542
586
|
LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
|
|
@@ -617,6 +661,86 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
|
|
|
617
661
|
|
|
618
662
|
</details>
|
|
619
663
|
|
|
664
|
+
## Managed cloud (OreCloud)
|
|
665
|
+
|
|
666
|
+
LodeDB stores can live in [OreCloud](https://db.egoistmachines.com), the managed-cloud
|
|
667
|
+
companion: durable sync for local stores, hosted read serving, and a per-user memory
|
|
668
|
+
data plane for agent applications. The client ships as the `[cloud]` extra:
|
|
669
|
+
|
|
670
|
+
```sh
|
|
671
|
+
pip install "lodedb[cloud]"
|
|
672
|
+
|
|
673
|
+
lodedb cloud login # one browser approval
|
|
674
|
+
lodedb cloud init ./my-store # link the directory to a managed remote
|
|
675
|
+
lodedb cloud keys ./my-store # list the directory's index keys
|
|
676
|
+
lodedb cloud sync ./my-store cloud <key> # mirror the local store to the cloud
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
A cloud store opens through the same class as a local one, via the
|
|
680
|
+
`LodeDB.cloud` alternate constructor:
|
|
681
|
+
|
|
682
|
+
```python
|
|
683
|
+
from lodedb import LodeDB
|
|
684
|
+
|
|
685
|
+
db = LodeDB("./notes") # local, as always
|
|
686
|
+
db = LodeDB.cloud("user-42") # managed cloud, same verbs
|
|
687
|
+
db.add("the quick brown fox") # embedded server-side
|
|
688
|
+
db.search("fox", k=5)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
A bare store id is enough. The org/environment half resolves from the
|
|
692
|
+
credential (`token=`, the `ORECLOUD_TOKEN` environment variable, or
|
|
693
|
+
`lodedb cloud login`); pass the full `"org/environment/store"` triple in
|
|
694
|
+
cross-environment scripts. For config-driven code, where one string field (an
|
|
695
|
+
env var, a YAML value) must express either a local path or a cloud store, the
|
|
696
|
+
plain constructor accepts the explicit URL form
|
|
697
|
+
`LodeDB("orecloud://org/environment/store")` and returns the same handle.
|
|
698
|
+
An application serving many end users should hold one `Client` and open
|
|
699
|
+
per-user handles from it (`Client().store(user_id)`); the credential resolves
|
|
700
|
+
once and the handles share one HTTP pool. `from lodedb.cloud import Client`
|
|
701
|
+
works as the import root.
|
|
702
|
+
|
|
703
|
+
Work locally with `lodedb` exactly as before. The client is first-party code
|
|
704
|
+
in this package (`lodedb.cloud` and the `lodedb cloud` CLI), and push/pull/
|
|
705
|
+
sync run on the same bundled native core as the engine. The extra installs
|
|
706
|
+
only its dependencies (`httpx`, `pynacl`). Everything cloud loads lazily, so
|
|
707
|
+
a plain `import lodedb` stays network-free.
|
|
708
|
+
|
|
709
|
+
### Sealed stores
|
|
710
|
+
|
|
711
|
+
OreCloud can host a server-side encrypted store whose 32-byte wrapping key
|
|
712
|
+
remains with your application. Install the additional lazy crypto support,
|
|
713
|
+
keep that material outside source control, and keep a recovery copy: losing
|
|
714
|
+
it makes the store unreadable and the service never persists it.
|
|
715
|
+
|
|
716
|
+
```sh
|
|
717
|
+
pip install "lodedb[cloud,cloud-sealed]"
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
```python
|
|
721
|
+
import secrets
|
|
722
|
+
|
|
723
|
+
from lodedb.cloud import Client
|
|
724
|
+
|
|
725
|
+
material = secrets.token_bytes(32)
|
|
726
|
+
client = Client()
|
|
727
|
+
client.create_store(
|
|
728
|
+
"user-42",
|
|
729
|
+
encrypted=True,
|
|
730
|
+
key_material=material,
|
|
731
|
+
mode="cloud_writer",
|
|
732
|
+
preset="minilm",
|
|
733
|
+
)
|
|
734
|
+
expires_at = client.unseal_store("user-42", material, ttl_seconds=900)
|
|
735
|
+
# Query or write while the grant is live, then remove it when finished.
|
|
736
|
+
client.reseal_store("user-42")
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
`Client.rotate_store_key("user-42", fresh_material)` replaces the caller-held
|
|
740
|
+
material after the store is unsealed. A sealed data-plane read returns
|
|
741
|
+
`CloudError` with status 423 and a `store_sealed:` detail; applications decide
|
|
742
|
+
when to request an unseal grant.
|
|
743
|
+
|
|
620
744
|
## Concurrency & durability
|
|
621
745
|
|
|
622
746
|
- **Single writer, many readers, per path.** One handle holds the path open for *writing* at
|
|
@@ -660,7 +784,7 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
|
|
|
660
784
|
read-your-writes against an append's returned LSN). On Windows the shared lock degrades to an
|
|
661
785
|
exclusive hold, so appenders serialize there rather than coexisting. A record is a precomputed
|
|
662
786
|
vector plus metadata (with an optional caption, e.g. for an image, retained only when the appender
|
|
663
|
-
opts into `store_text
|
|
787
|
+
opts into `store_text`; it is off by default, so no raw text reaches the WAL); with an embedder
|
|
664
788
|
configured, the appender also ingests full text (chunked by the core, embedded in the binding
|
|
665
789
|
layer, then logged as a post-embedding record) so text writes are multi-producer too, without a
|
|
666
790
|
captured base generation. It requires WAL commit mode. Exposed as the native `CoreAppender`, over
|
|
@@ -337,6 +337,42 @@ db.search("turbine fault", k=5) # nearest cluste
|
|
|
337
337
|
`ann_clusters` (partitions) defaults to about `sqrt(n)` and `ann_nprobe` (clusters probed per
|
|
338
338
|
query) to about `sqrt(clusters)`. ANN is a create-time choice persisted with the index and works
|
|
339
339
|
for text and bring-your-own-vector indexes alike; keep the exact default for small-to-mid corpora.
|
|
340
|
+
On reopen, `ann_nprobe` is a session-only override, so benchmark handles can change probe breadth
|
|
341
|
+
without rebuilding; the algorithm and partition count remain the persisted configuration.
|
|
342
|
+
|
|
343
|
+
For large corpora, these are useful starting ranges to measure, not recall or latency promises:
|
|
344
|
+
|
|
345
|
+
| Knob | Useful range | Trade-off |
|
|
346
|
+
| --- | --- | --- |
|
|
347
|
+
| `ann_nprobe` | 64 to 256 clusters | More probes raise recall and scan work; probing every cluster is exact. |
|
|
348
|
+
| `rescore_oversample` | 2 to 8 times `k` | More candidates improve the chance that fp32 re-ranking repairs compact-code errors, at more sidecar reads. |
|
|
349
|
+
</details>
|
|
350
|
+
|
|
351
|
+
<details>
|
|
352
|
+
<summary><b>Two-stage rescore (`rescore="original"`)</b></summary>
|
|
353
|
+
|
|
354
|
+
TurboVec's compact 4-bit scan is fast, but its quantized first-stage ranking can put close
|
|
355
|
+
neighbors in the wrong order. Enable rescore at creation to retain each input vector in a separate
|
|
356
|
+
original-precision sidecar, then re-score the first-stage candidate pool with fp32 dot products:
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
db = LodeDB(
|
|
360
|
+
path="./data",
|
|
361
|
+
ann="cluster",
|
|
362
|
+
ann_clusters=1024,
|
|
363
|
+
rescore="original",
|
|
364
|
+
rescore_dtype="float16", # default; about 2 bytes per dimension per vector
|
|
365
|
+
rescore_oversample=4,
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`float16` sidecars add about 2 bytes per dimension per vector (`float32` adds 4; `int8` adds 1).
|
|
370
|
+
They let the final ordering rise above the practical 4-bit ranking ceiling while retaining the
|
|
371
|
+
compact index for candidate generation. Re-scored candidate scores are exact fp32 dots, but the
|
|
372
|
+
overall result can still be approximate when ANN or a too-small candidate pool excludes a true
|
|
373
|
+
neighbor. Rescore is a create-time capture choice: originals are not recoverable after a normal
|
|
374
|
+
store's first ingest, so enabling it later requires a rebuild. On a rescore-enabled reopen,
|
|
375
|
+
`rescore_oversample` is a session-only override; mode and dtype must match the persisted sidecar.
|
|
340
376
|
</details>
|
|
341
377
|
|
|
342
378
|
## Multimodal & bring-your-own vectors
|
|
@@ -486,6 +522,9 @@ lodedb mcp # stdio MCP server for agent memory
|
|
|
486
522
|
lodedb benchmark # local, metrics-only benchmark
|
|
487
523
|
```
|
|
488
524
|
|
|
525
|
+
Use `lodedb serve` as a local OpenAI-compatible embeddings provider; see [LodeDB as a local
|
|
526
|
+
embeddings provider](docs/integrations.md#lodedb-as-a-local-embeddings-provider).
|
|
527
|
+
|
|
489
528
|
## Use as an MCP server
|
|
490
529
|
|
|
491
530
|
LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
|
|
@@ -566,6 +605,86 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
|
|
|
566
605
|
|
|
567
606
|
</details>
|
|
568
607
|
|
|
608
|
+
## Managed cloud (OreCloud)
|
|
609
|
+
|
|
610
|
+
LodeDB stores can live in [OreCloud](https://db.egoistmachines.com), the managed-cloud
|
|
611
|
+
companion: durable sync for local stores, hosted read serving, and a per-user memory
|
|
612
|
+
data plane for agent applications. The client ships as the `[cloud]` extra:
|
|
613
|
+
|
|
614
|
+
```sh
|
|
615
|
+
pip install "lodedb[cloud]"
|
|
616
|
+
|
|
617
|
+
lodedb cloud login # one browser approval
|
|
618
|
+
lodedb cloud init ./my-store # link the directory to a managed remote
|
|
619
|
+
lodedb cloud keys ./my-store # list the directory's index keys
|
|
620
|
+
lodedb cloud sync ./my-store cloud <key> # mirror the local store to the cloud
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
A cloud store opens through the same class as a local one, via the
|
|
624
|
+
`LodeDB.cloud` alternate constructor:
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
from lodedb import LodeDB
|
|
628
|
+
|
|
629
|
+
db = LodeDB("./notes") # local, as always
|
|
630
|
+
db = LodeDB.cloud("user-42") # managed cloud, same verbs
|
|
631
|
+
db.add("the quick brown fox") # embedded server-side
|
|
632
|
+
db.search("fox", k=5)
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
A bare store id is enough. The org/environment half resolves from the
|
|
636
|
+
credential (`token=`, the `ORECLOUD_TOKEN` environment variable, or
|
|
637
|
+
`lodedb cloud login`); pass the full `"org/environment/store"` triple in
|
|
638
|
+
cross-environment scripts. For config-driven code, where one string field (an
|
|
639
|
+
env var, a YAML value) must express either a local path or a cloud store, the
|
|
640
|
+
plain constructor accepts the explicit URL form
|
|
641
|
+
`LodeDB("orecloud://org/environment/store")` and returns the same handle.
|
|
642
|
+
An application serving many end users should hold one `Client` and open
|
|
643
|
+
per-user handles from it (`Client().store(user_id)`); the credential resolves
|
|
644
|
+
once and the handles share one HTTP pool. `from lodedb.cloud import Client`
|
|
645
|
+
works as the import root.
|
|
646
|
+
|
|
647
|
+
Work locally with `lodedb` exactly as before. The client is first-party code
|
|
648
|
+
in this package (`lodedb.cloud` and the `lodedb cloud` CLI), and push/pull/
|
|
649
|
+
sync run on the same bundled native core as the engine. The extra installs
|
|
650
|
+
only its dependencies (`httpx`, `pynacl`). Everything cloud loads lazily, so
|
|
651
|
+
a plain `import lodedb` stays network-free.
|
|
652
|
+
|
|
653
|
+
### Sealed stores
|
|
654
|
+
|
|
655
|
+
OreCloud can host a server-side encrypted store whose 32-byte wrapping key
|
|
656
|
+
remains with your application. Install the additional lazy crypto support,
|
|
657
|
+
keep that material outside source control, and keep a recovery copy: losing
|
|
658
|
+
it makes the store unreadable and the service never persists it.
|
|
659
|
+
|
|
660
|
+
```sh
|
|
661
|
+
pip install "lodedb[cloud,cloud-sealed]"
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
```python
|
|
665
|
+
import secrets
|
|
666
|
+
|
|
667
|
+
from lodedb.cloud import Client
|
|
668
|
+
|
|
669
|
+
material = secrets.token_bytes(32)
|
|
670
|
+
client = Client()
|
|
671
|
+
client.create_store(
|
|
672
|
+
"user-42",
|
|
673
|
+
encrypted=True,
|
|
674
|
+
key_material=material,
|
|
675
|
+
mode="cloud_writer",
|
|
676
|
+
preset="minilm",
|
|
677
|
+
)
|
|
678
|
+
expires_at = client.unseal_store("user-42", material, ttl_seconds=900)
|
|
679
|
+
# Query or write while the grant is live, then remove it when finished.
|
|
680
|
+
client.reseal_store("user-42")
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
`Client.rotate_store_key("user-42", fresh_material)` replaces the caller-held
|
|
684
|
+
material after the store is unsealed. A sealed data-plane read returns
|
|
685
|
+
`CloudError` with status 423 and a `store_sealed:` detail; applications decide
|
|
686
|
+
when to request an unseal grant.
|
|
687
|
+
|
|
569
688
|
## Concurrency & durability
|
|
570
689
|
|
|
571
690
|
- **Single writer, many readers, per path.** One handle holds the path open for *writing* at
|
|
@@ -609,7 +728,7 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
|
|
|
609
728
|
read-your-writes against an append's returned LSN). On Windows the shared lock degrades to an
|
|
610
729
|
exclusive hold, so appenders serialize there rather than coexisting. A record is a precomputed
|
|
611
730
|
vector plus metadata (with an optional caption, e.g. for an image, retained only when the appender
|
|
612
|
-
opts into `store_text
|
|
731
|
+
opts into `store_text`; it is off by default, so no raw text reaches the WAL); with an embedder
|
|
613
732
|
configured, the appender also ingests full text (chunked by the core, embedded in the binding
|
|
614
733
|
layer, then logged as a post-embedding record) so text writes are multi-producer too, without a
|
|
615
734
|
captured base generation. It requires WAL commit mode. Exposed as the native `CoreAppender`, over
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "lodedb-cloud-core"
|
|
3
|
+
description = "Native transfer core for the LodeDB managed cloud — durable push/pull/sync over the open commit format."
|
|
4
|
+
version.workspace = true
|
|
5
|
+
edition.workspace = true
|
|
6
|
+
rust-version.workspace = true
|
|
7
|
+
license.workspace = true
|
|
8
|
+
repository.workspace = true
|
|
9
|
+
publish = false
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
# The engine core. The cloud transfer plane links its `storage` module directly
|
|
13
|
+
# so the managed cloud and the embedded product share ONE commit-format
|
|
14
|
+
# implementation (schema, layout, checksum, sub-manifests incl. `tvmv`). A path
|
|
15
|
+
# dep now that both live in this workspace — engine changes and the transfer
|
|
16
|
+
# plane move together under one suite.
|
|
17
|
+
lodedb-core = { path = "../lodedb-core" }
|
|
18
|
+
# Object-storage backend (S3/GCS/Azure) behind the same ArtifactStore trait. The
|
|
19
|
+
# `aws` feature adds the S3 builder; the crate's conditional-put API (Create /
|
|
20
|
+
# Update{e_tag}) is what makes the root-pointer compare-and-swap strongly
|
|
21
|
+
# consistent on object storage. Its InMemory backend also drives the tests.
|
|
22
|
+
object_store = { version = "0.12.5", features = ["aws"] }
|
|
23
|
+
serde_json = "1.0"
|
|
24
|
+
sha2 = "0.10"
|
|
25
|
+
# Streaming transfer plumbing: `bytes` for the object-store chunk type and
|
|
26
|
+
# `futures` for driving its download streams chunk-by-chunk from the sync
|
|
27
|
+
# ArtifactStore surface. Both are already in the tree via object_store.
|
|
28
|
+
bytes = "1"
|
|
29
|
+
futures = "0.3"
|
|
30
|
+
# The object_store backends are async; the runtime is enabled with net + time so a
|
|
31
|
+
# real S3 client works (not just the in-memory test backend). ObjectArtifactStore
|
|
32
|
+
# keeps the sync ArtifactStore trait by owning a current-thread runtime.
|
|
33
|
+
tokio = { version = "1.47.5", default-features = false, features = ["rt", "net", "time"] }
|
|
34
|
+
# The pointer serialize/validate round-trip reuses lodedb-core's own
|
|
35
|
+
# write/read_commit_manifest (which operate on file paths) via a scratch file, so
|
|
36
|
+
# object-store pointer documents are byte-identical to on-disk ones. Also used by
|
|
37
|
+
# the test fixtures to build committed generations in throwaway directories.
|
|
38
|
+
tempfile = "3"
|
|
39
|
+
|
|
40
|
+
[lints]
|
|
41
|
+
workspace = true
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
//! Storage abstraction for committed, generation-addressed artifacts.
|
|
2
|
+
//!
|
|
3
|
+
//! The substrate is the engine's existing on-disk layout: a committed generation
|
|
4
|
+
//! is a set of immutable, sha256-addressed `g<epoch>.*` artifacts under
|
|
5
|
+
//! `<key>.gen/` pinned by an atomic `<key>.commit.json` root pointer. An
|
|
6
|
+
//! [`ArtifactStore`] exposes exactly the operations that layout needs:
|
|
7
|
+
//!
|
|
8
|
+
//! - **names** are store-relative paths mirroring the on-disk layout, e.g.
|
|
9
|
+
//! `idx.gen/g7.json` or `idx.gen/g7.json.json-delta/delta-00000000.jsd`;
|
|
10
|
+
//! - the **pointer key** is the index key (`idx` -> `idx.commit.json`).
|
|
11
|
+
//!
|
|
12
|
+
//! Implementations treat content artifacts as immutable and content-addressed
|
|
13
|
+
//! (written once, never overwritten), and commit a generation *only* by swapping
|
|
14
|
+
//! the root pointer. [`LocalArtifactStore`](crate::LocalArtifactStore) is the
|
|
15
|
+
//! default; object-storage backends (S3/GCS/Azure) land in later milestones with
|
|
16
|
+
//! the artifact names becoming object keys under a per-tenant prefix and the
|
|
17
|
+
//! pointer swap becoming a conditional write.
|
|
18
|
+
|
|
19
|
+
use crate::error::Result;
|
|
20
|
+
use serde_json::Value;
|
|
21
|
+
use std::io::Read;
|
|
22
|
+
|
|
23
|
+
/// Reads/writes immutable artifacts and swaps a root pointer atomically.
|
|
24
|
+
///
|
|
25
|
+
/// The interface is deliberately small: streaming artifact I/O plus a pointer
|
|
26
|
+
/// compare-and-swap. That is everything the generation-addressed commit format
|
|
27
|
+
/// needs as a cloud substrate. Streaming is the primitive. Vector bases run
|
|
28
|
+
/// to gigabytes, so a transfer's peak memory must be a fixed buffer, never a
|
|
29
|
+
/// function of artifact size; the buffered methods are conveniences for the
|
|
30
|
+
/// small payloads (pointer documents) built on top.
|
|
31
|
+
pub trait ArtifactStore {
|
|
32
|
+
/// Opens one artifact for streaming reads;
|
|
33
|
+
/// [`ArtifactStoreError::NotFound`] if the name is absent.
|
|
34
|
+
///
|
|
35
|
+
/// [`ArtifactStoreError::NotFound`]: crate::ArtifactStoreError::NotFound
|
|
36
|
+
fn open_read<'a>(&'a self, name: &str) -> Result<Box<dyn Read + 'a>>;
|
|
37
|
+
|
|
38
|
+
/// Returns one artifact's bytes, fully buffered, for small payloads
|
|
39
|
+
/// only; transfers and verification stream via [`open_read`](Self::open_read).
|
|
40
|
+
fn read_bytes(&self, name: &str) -> Result<Vec<u8>> {
|
|
41
|
+
let mut data = Vec::new();
|
|
42
|
+
self.open_read(name)?.read_to_end(&mut data)?;
|
|
43
|
+
Ok(data)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Streams one immutable artifact into the store unless it already
|
|
47
|
+
/// exists, hashing incrementally as it copies.
|
|
48
|
+
///
|
|
49
|
+
/// `sha256` is the expected lowercase-hex digest of the streamed bytes; a
|
|
50
|
+
/// mismatch is an integrity error and nothing is stored, so corruption can
|
|
51
|
+
/// never land, even when the source is another store's live stream. A
|
|
52
|
+
/// name already present with identical bytes is a no-op (idempotent
|
|
53
|
+
/// re-push); present with *different* bytes is a conflict. Artifacts are
|
|
54
|
+
/// immutable and are never overwritten in place. On the already-present
|
|
55
|
+
/// no-op path the incoming stream may be left partially (or wholly)
|
|
56
|
+
/// unread and unvalidated: success then attests that the STORED bytes
|
|
57
|
+
/// match `sha256`, not that the stream did.
|
|
58
|
+
///
|
|
59
|
+
/// `size_hint` is the expected byte count (0 = unknown), advisory only:
|
|
60
|
+
/// backends use it to pick an upload strategy (the object store's
|
|
61
|
+
/// conditional-claim path has a provider copy-size ceiling), never to
|
|
62
|
+
/// trust the stream's length. Transfers pass the manifest-recorded size.
|
|
63
|
+
fn write_stream_if_absent(
|
|
64
|
+
&self,
|
|
65
|
+
name: &str,
|
|
66
|
+
data: &mut dyn Read,
|
|
67
|
+
sha256: &str,
|
|
68
|
+
size_hint: u64,
|
|
69
|
+
) -> Result<()>;
|
|
70
|
+
|
|
71
|
+
/// Buffered convenience over
|
|
72
|
+
/// [`write_stream_if_absent`](Self::write_stream_if_absent), for small
|
|
73
|
+
/// payloads a caller already holds in memory.
|
|
74
|
+
fn write_bytes_if_absent(&self, name: &str, data: &[u8], sha256: &str) -> Result<()> {
|
|
75
|
+
let mut cursor = data;
|
|
76
|
+
self.write_stream_if_absent(name, &mut cursor, sha256, data.len() as u64)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Whether an artifact named `name` is present, without asserting anything
|
|
80
|
+
/// about its content.
|
|
81
|
+
///
|
|
82
|
+
/// The default opens the artifact and maps
|
|
83
|
+
/// [`NotFound`](crate::ArtifactStoreError::NotFound) to `false`; backends
|
|
84
|
+
/// with a cheap existence primitive (a filesystem stat, an object-store
|
|
85
|
+
/// HEAD) should override it to avoid opening a stream.
|
|
86
|
+
fn contains(&self, name: &str) -> Result<bool> {
|
|
87
|
+
match self.open_read(name) {
|
|
88
|
+
Ok(_) => Ok(true),
|
|
89
|
+
Err(crate::ArtifactStoreError::NotFound(_)) => Ok(false),
|
|
90
|
+
Err(error) => Err(error),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Returns the committed root-manifest body for `key`, or `None`.
|
|
95
|
+
///
|
|
96
|
+
/// The body carries the generation number, the base epoch, counts, and the
|
|
97
|
+
/// per-store sub-manifests that name every artifact in the generation. It is
|
|
98
|
+
/// the read side that [`compare_and_swap_pointer`](Self::compare_and_swap_pointer)
|
|
99
|
+
/// and the inventory helpers need to learn the currently-committed generation.
|
|
100
|
+
fn read_pointer(&self, key: &str) -> Result<Option<Value>>;
|
|
101
|
+
|
|
102
|
+
/// Publishes `new_body` as the root pointer iff the currently committed body is
|
|
103
|
+
/// exactly `old_body` (`None` means the pointer must not yet exist); otherwise
|
|
104
|
+
/// [`ArtifactStoreError::PointerConflict`]. This swap is the only commit point:
|
|
105
|
+
/// artifacts are published first, then the pointer flips all-or-nothing.
|
|
106
|
+
///
|
|
107
|
+
/// The precondition is the full committed body, not just its generation number.
|
|
108
|
+
/// A generation number is not a unique version token (two independent lineages
|
|
109
|
+
/// can share one with different content), so comparing the whole body is what
|
|
110
|
+
/// makes this a sound compare-and-swap rather than an ABA-prone check.
|
|
111
|
+
///
|
|
112
|
+
/// [`ArtifactStoreError::PointerConflict`]: crate::ArtifactStoreError::PointerConflict
|
|
113
|
+
fn compare_and_swap_pointer(
|
|
114
|
+
&self,
|
|
115
|
+
key: &str,
|
|
116
|
+
old_body: Option<&Value>,
|
|
117
|
+
new_body: &Value,
|
|
118
|
+
) -> Result<()>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The generation number recorded in a committed body, if present.
|
|
122
|
+
///
|
|
123
|
+
/// Used only to annotate a [`PointerConflict`](crate::ArtifactStoreError::PointerConflict)
|
|
124
|
+
/// for readability; the swap precondition is the full body, not this number.
|
|
125
|
+
pub(crate) fn body_generation(body: &Value) -> Option<u64> {
|
|
126
|
+
body.get("generation").and_then(Value::as_u64)
|
|
127
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//! Content-addressed blob naming for the managed remote layout.
|
|
2
|
+
//!
|
|
3
|
+
//! A managed remote can store artifacts content-addressed
|
|
4
|
+
//! (`blobs/sha256/aa/<sha256>` under a per-tenant prefix) instead of under
|
|
5
|
+
//! their engine path names. Content addressing is what absorbs the
|
|
6
|
+
//! fork-collision case (two branches committing different artifacts under the
|
|
7
|
+
//! same engine name, e.g. `idx.gen/g7.json`, coexist as two blobs), and the
|
|
8
|
+
//! two-level `aa/` fan-out keeps listings and prefix operations tractable.
|
|
9
|
+
//!
|
|
10
|
+
//! These helpers are pure and tested; they pin the naming contract the
|
|
11
|
+
//! transfer plane builds on. The digest is always the lowercase-hex
|
|
12
|
+
//! SHA-256 of the blob's bytes, the same digest the engine records per
|
|
13
|
+
//! artifact and [`ArtifactStore::write_bytes_if_absent`] verifies.
|
|
14
|
+
//!
|
|
15
|
+
//! [`ArtifactStore::write_bytes_if_absent`]: crate::ArtifactStore::write_bytes_if_absent
|
|
16
|
+
|
|
17
|
+
use crate::error::{ArtifactStoreError, Result};
|
|
18
|
+
|
|
19
|
+
/// The fixed prefix every blob name lives under.
|
|
20
|
+
const BLOB_PREFIX: &str = "blobs/sha256";
|
|
21
|
+
|
|
22
|
+
/// Returns the store-relative blob name for a content digest:
|
|
23
|
+
/// `blobs/sha256/aa/<sha256>`, where `aa` is the digest's first two hex chars.
|
|
24
|
+
///
|
|
25
|
+
/// Rejects anything that is not exactly 64 lowercase hex characters. Blob
|
|
26
|
+
/// names participate in authorization paths, so a malformed digest must fail
|
|
27
|
+
/// closed rather than mint a name outside the contract.
|
|
28
|
+
pub fn blob_name(sha256: &str) -> Result<String> {
|
|
29
|
+
validate_sha256(sha256)?;
|
|
30
|
+
Ok(format!("{BLOB_PREFIX}/{}/{sha256}", &sha256[..2]))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Parses a blob name back to its content digest, the exact inverse of
|
|
34
|
+
/// [`blob_name`]. Rejects any name that deviates from the contract (wrong
|
|
35
|
+
/// prefix, fan-out directory disagreeing with the digest, malformed digest).
|
|
36
|
+
pub fn parse_blob_name(name: &str) -> Result<String> {
|
|
37
|
+
let malformed = || {
|
|
38
|
+
ArtifactStoreError::Integrity(format!(
|
|
39
|
+
"blob name {name:?} does not match {BLOB_PREFIX}/aa/<sha256>"
|
|
40
|
+
))
|
|
41
|
+
};
|
|
42
|
+
let rest = name.strip_prefix(BLOB_PREFIX).ok_or_else(malformed)?;
|
|
43
|
+
let rest = rest.strip_prefix('/').ok_or_else(malformed)?;
|
|
44
|
+
let (fan_out, sha256) = rest.split_once('/').ok_or_else(malformed)?;
|
|
45
|
+
validate_sha256(sha256).map_err(|_| malformed())?;
|
|
46
|
+
if fan_out != &sha256[..2] {
|
|
47
|
+
return Err(malformed());
|
|
48
|
+
}
|
|
49
|
+
Ok(sha256.to_string())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Requires exactly 64 lowercase hex characters. Also used by the generation
|
|
53
|
+
/// inventory to validate every manifest digest at the trust boundary: digests
|
|
54
|
+
/// become staging file names and object keys, so a non-hex "digest" must fail
|
|
55
|
+
/// closed before it can name a path.
|
|
56
|
+
pub(crate) fn validate_sha256(sha256: &str) -> Result<()> {
|
|
57
|
+
let valid = sha256.len() == 64
|
|
58
|
+
&& sha256
|
|
59
|
+
.bytes()
|
|
60
|
+
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
|
|
61
|
+
if valid {
|
|
62
|
+
Ok(())
|
|
63
|
+
} else {
|
|
64
|
+
Err(ArtifactStoreError::Integrity(format!(
|
|
65
|
+
"{sha256:?} is not a lowercase-hex sha256 digest"
|
|
66
|
+
)))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(test)]
|
|
71
|
+
mod tests {
|
|
72
|
+
use super::{blob_name, parse_blob_name};
|
|
73
|
+
use crate::error::ArtifactStoreError;
|
|
74
|
+
|
|
75
|
+
const SHA: &str = "aa30b1cc05c10ac8a1f4e6d46f7d4b1a9c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f";
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn names_a_blob_under_its_two_char_fan_out() {
|
|
79
|
+
assert_eq!(blob_name(SHA).unwrap(), format!("blobs/sha256/aa/{SHA}"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn parse_inverts_blob_name() {
|
|
84
|
+
assert_eq!(parse_blob_name(&blob_name(SHA).unwrap()).unwrap(), SHA);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn rejects_malformed_digests() {
|
|
89
|
+
for bad in [
|
|
90
|
+
"",
|
|
91
|
+
"abc",
|
|
92
|
+
&SHA[..63], // too short
|
|
93
|
+
&format!("{SHA}0"), // too long
|
|
94
|
+
&SHA.to_uppercase(), // uppercase
|
|
95
|
+
&format!("g{}", &SHA[1..]), // non-hex
|
|
96
|
+
] {
|
|
97
|
+
assert!(matches!(
|
|
98
|
+
blob_name(bad).unwrap_err(),
|
|
99
|
+
ArtifactStoreError::Integrity(_)
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn rejects_names_off_contract() {
|
|
106
|
+
for bad in [
|
|
107
|
+
format!("blobs/sha256/{SHA}"), // no fan-out level
|
|
108
|
+
format!("blobs/sha256/bb/{SHA}"), // fan-out disagrees
|
|
109
|
+
format!("blobs/md5/aa/{SHA}"), // wrong algorithm segment
|
|
110
|
+
format!("sha256/aa/{SHA}"), // wrong prefix
|
|
111
|
+
format!("blobs/sha256/aa/{}", &SHA[..63]), // malformed digest
|
|
112
|
+
] {
|
|
113
|
+
assert!(matches!(
|
|
114
|
+
parse_blob_name(&bad).unwrap_err(),
|
|
115
|
+
ArtifactStoreError::Integrity(_)
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|