Adds a second external storage backend (Postgres/pgvector) alongside Qdrant to prove the BaseBackend/BaseCollection contract generalizes across substrates (SQL + JSONB containment filters + pgvector `<=>` ranking vs Qdrant's REST/dict model), and addresses the review feedback on PR #1679. Backend (mempalace/backends/pgvector.py): - table-per-(namespace, palace, collection) isolation; advertises supports_namespace_isolation - JSONB filter pushdown for the containment subset, local-exact fallback for $or/$contains/comparisons/where_document - BM25 lexical search; marker-based mismatch protection - optional psycopg dependency (lazy import), in-memory fake for CI, live test gated on MEMPALACE_PGVECTOR_LIVE_URL - registered in registry/__init__/pyproject entry point + [pgvector] extra; MEMPALACE_PGVECTOR_DSN / MEMPALACE_PGVECTOR_NAMESPACE config; README docs Isolation contract (RFC 001): - PalaceRef/BaseBackend document the per-id MUST and the cross-namespace MUST, gated on the new supports_namespace_isolation capability token - runnable conformance suite (tests/_backend_conformance.py, tests/test_backend_conformance.py); qdrant + pgvector run it via their fakes Marker fail-loud guard: - qdrant and pgvector now refuse get_collection when local_path is None instead of silently opening a remote collection with no mismatch protection Review fixes: - palace._open_collection_or_explain handles unknown-backend KeyError as a CLI state message instead of an escaping stack trace - dedup.py docstring no longer claims "No API calls" unconditionally (false for remote backends)
44 lines
1.6 KiB
Python
44 lines
1.6 KiB
Python
"""Backend isolation conformance suite (RFC 001 isolation contract).
|
|
|
|
Runs the shared isolation assertions from ``_backend_conformance`` against the
|
|
built-in local backends. Server-mode backends (qdrant) run the same assertions
|
|
under their own fake/live client in ``test_qdrant_backend.py``.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from _backend_conformance import assert_partition_isolation
|
|
|
|
from mempalace.backends import PalaceRef
|
|
from mempalace.backends.chroma import ChromaBackend
|
|
from mempalace.backends.sqlite_exact import SQLiteExactBackend
|
|
|
|
_LOCAL_BACKENDS = [
|
|
pytest.param(ChromaBackend, id="chroma"),
|
|
pytest.param(SQLiteExactBackend, id="sqlite_exact"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("backend_cls", _LOCAL_BACKENDS)
|
|
def test_cross_palace_isolation(backend_cls, tmp_path):
|
|
"""One backend instance must isolate two distinct palaces (PalaceRef.id)."""
|
|
backend = backend_cls()
|
|
try:
|
|
cols = []
|
|
for label in ("alpha", "beta"):
|
|
path = tmp_path / label
|
|
ref = PalaceRef(id=str(path), local_path=str(path))
|
|
cols.append(
|
|
backend.get_collection(palace=ref, collection_name="mempalace_drawers", create=True)
|
|
)
|
|
assert_partition_isolation(backend, cols[0], cols[1])
|
|
finally:
|
|
backend.close()
|
|
|
|
|
|
def test_local_backends_do_not_claim_namespace_isolation():
|
|
"""Local backends isolate by on-disk path, not namespace; they must not
|
|
advertise the namespace-isolation capability (RFC 001 isolation contract)."""
|
|
assert "supports_namespace_isolation" not in ChromaBackend.capabilities
|
|
assert "supports_namespace_isolation" not in SQLiteExactBackend.capabilities
|