fix(backends): require SQLite magic header for chroma + sqlite_exact detect() (#1893) - #1896
Merged
igorls merged 2 commits intoJun 28, 2026
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes #1893.
ChromaBackend.detect()andSQLiteExactBackend.detect()were both returningTruefor a 0-byte SQLite marker file (chroma.sqlite3/sqlite_exact.sqlite3) because each check was justos.path.isfile(...). On a palace that has any other backend marker alongside one of those stale 0-byte files,resolve_backend_namethen raisesBackendMismatchError: palace at '<path>' contains multiple backend artifacts: <a>, <b>and the palace becomes unopenable until the user manuallyrms the empty file.The 0-byte file appears as a side effect of any
sqlite3.connect()on a missing path — Python creates the file immediately but writes the SQLite header only on the first statement. So any code path that touches the marker path with baresqlite3.connect(), including chromadb's ownPersistentClientlazy-init (see the comment atbackends/chroma.py:2052), can leave a 0-byte artifact behind.Change
Both
ChromaBackend.detectandSQLiteExactBackend.detectnow check the SQLite magic header (b"SQLite format 3\x00", the first 16 bytes) instead of file presence alone. One extraopen()+ 16-byteread()per backend — detection isn't a hot path.Properties of the new check (identical for both backends):
CREATE TABLEis enough to land the header)detect()to any backend-specific schema — magic header is stable across chromadb releases / sqlite_exact schema changesBoth backends fixed together for repo-wide consistency, per the review request on the initial chroma-only revision of this PR.
Test sweep — context for the larger-than-expected diff
Many existing tests used
(<path> / "chroma.sqlite3").touch()or.write_bytes(b"")(and one site(<path> / "sqlite_exact.sqlite3").write_bytes(b"")) as a "fake palace" shortcut, exploiting the looseos.path.isfile()semantics. One of these sites even has the comment:— signaling that the convention was a known shortcut, not a contract. After the magic-header check, those stand-ins no longer register, so each call site needed to create a real (if minimal) SQLite database.
To avoid duplicating the same
sqlite3.connect + CREATE TABLEboilerplate, this PR introducestests/_chroma_palace_helper.pyfollowing the existingtests/_backend_conformance.pyprecedent for private-module test utilities. It exports:make_minimal_chroma_sqlite(palace_path)—chroma.sqlite3with headermake_minimal_sqlite_exact_sqlite(palace_path)—sqlite_exact.sqlite3with headerBoth wrap a shared
_write_minimal_sqlite_file(db_path)private helper.Each previously-shortcut call site now reads
make_minimal_<...>_sqlite(palace)— same one-line shape, just honest about producing a real SQLite file.Files updated for the sweep:
test_backends.py,test_daemon.py,test_mcp_server.py,test_palace.py(4 sites),test_qdrant_backend.py,test_repair.py(3 sites),test_searcher.py,test_sqlite_exact_backend.py(3 sites),test_sync.py(2 sites).test_migrate.pyalso writes tochroma.sqlite3paths, but with non-empty payloads (b"original") used to verify rename/relocation behavior, not detection. Those tests don't exercise the detect path and continue to work unchanged.How to test
For each backend the trio of dedicated detection tests:
chroma:
test_chroma_detect_matches_palace_with_sqlite_headertest_chroma_detect_rejects_empty_chroma_sqlitetest_chroma_detect_rejects_non_sqlite_filesqlite_exact:
test_sqlite_exact_detect_matches_palace_with_sqlite_headertest_sqlite_exact_detect_rejects_empty_sqlite_exact_sqlitetest_sqlite_exact_detect_rejects_non_sqlite_fileFull env-cleared suite: 3140 passed, 20 skipped, 0 failed.
ruff check .andruff format --check .both clean.Checklist
uv run pytest tests/ -v --ignore=tests/benchmarks)uv run ruff check .)uv run ruff format --check .)