Skip to content

fix(vector_store): make VectorManager methods work on persistent backends (#855) - #914

Merged
KaifAhmad1 merged 5 commits into
semantica-agi:mainfrom
yunaremaia:fix/855-vectormanager-persistent-count
Aug 13, 2026
Merged

fix(vector_store): make VectorManager methods work on persistent backends (#855)#914
KaifAhmad1 merged 5 commits into
semantica-agi:mainfrom
yunaremaia:fix/855-vectormanager-persistent-count

Conversation

@yunaremaia

Copy link
Copy Markdown
Contributor

Closes #855

Problem

VectorManager.maintain_store() and VectorManager.collect_statistics() reached into VectorStore internals (store.vectors / store.metadata), which are only initialized for the inmemory backend. Any persistent backend (FAISS, Qdrant, Pinecone, Milvus, ...) never sets them, so both methods crashed immediately with AttributeError — they were unusable outside the inmemory backend.

Fix

Tests

10 hermetic unit tests (no external services): inmemory count, empty store, delegation to a backend count(), NotImplementedError path, and both VectorManager methods against inmemory, counting and non-counting persistent-style stores — including a regression assertion that the failure mode is NotImplementedError, not AttributeError.

Core vector_store suite: 40 passed. (Pre-existing environment failures in test_kg_integration.py / test_decision_embedding_pipeline.py are unrelated — they fail identically on main without this change.)

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix VectorManager health/stats on persistent vector stores via VectorStore.count()

🐞 Bug fix 🧪 Tests ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Add backend-agnostic VectorStore.count() with clear NotImplementedError on unsupported backends
• Update VectorManager health/stats to use count() instead of inmemory-only internals
• Add regression tests covering inmemory, delegating backends, and missing-count behavior
Diagram

graph TD
  VM["VectorManager"] --> VS["VectorStore.count()"] --> D1{"Backend inmemory?"}
  D1 -->|"yes"| IM["Inmemory dicts"] --> RC["Return count"]
  D1 -->|"no"| BS["Persistent backend store"] --> D2{"count() supported?"}
  D2 -->|"yes"| RC
  D2 -->|"no"| NIE["NotImplementedError"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Require count() across all persistent backend adapters
  • ➕ Uniform capability across backends; fewer conditional branches at call sites
  • ➕ Avoids NotImplementedError behavior differences between backends
  • ➖ Higher scope: requires touching every backend integration (and potentially external service calls)
  • ➖ Harder to keep tests hermetic; may force expensive remote count operations
2. Move health/stats logic into VectorStore (single abstraction boundary)
  • ➕ VectorManager stays thin; VectorStore owns backend-specific semantics
  • ➕ Easier to maintain invariants around metadata/vector counting behavior
  • ➖ Shifts responsibilities and could expand VectorStore’s surface area beyond storage concerns
  • ➖ Still needs a count-like capability; doesn’t eliminate the underlying feature gap
3. Introduce a formal backend capability interface (Protocol/ABC)

Recommendation: The PR’s approach—adding a backend-agnostic VectorStore.count() with explicit NotImplementedError when unsupported—is the best tradeoff for #855. It fixes the immediate crash, preserves inmemory semantics in maintain_store(), and provides a clear capability signal for persistent backends without forcing a broad backend-interface refactor.

Files changed (2) +158 / -4

Bug fix (1) +34 / -4
vector_store.pyAdd VectorStore.count() and use it in VectorManager health/stats +34/-4

Add VectorStore.count() and use it in VectorManager health/stats

• Introduces a public VectorStore.count() accessor that counts inmemory vectors, delegates to persistent backend stores that implement count(), and raises NotImplementedError when counting isn’t supported. Updates VectorManager.maintain_store() and collect_statistics() to use count() instead of accessing inmemory-only .vectors/.metadata internals, preserving separate metadata counting for inmemory while using 1:1 counts for persistent stores.

semantica/vector_store/vector_store.py

Tests (1) +124 / -0
test_vector_manager_persistent.pyRegression tests for persistent-backend VectorManager counting (#855) +124/-0

Regression tests for persistent-backend VectorManager counting (#855)

• Adds hermetic unit tests for VectorStore.count() across inmemory, delegating persistent-style backends, and the unsupported-count NotImplementedError path. Adds regression coverage ensuring VectorManager.collect_statistics() and maintain_store() work for persistent-style stores and fail with NotImplementedError (not AttributeError) when counting isn’t available.

tests/vector_store/test_vector_manager_persistent.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Silent zero count ✓ Resolved 🐞 Bug ≡ Correctness
Description
VectorStore.count() returns 0 when backend != "inmemory" and _backend_store is None, which makes
VectorManager.collect_statistics()/maintain_store() report an empty, healthy store instead of
surfacing that the persistent backend isn’t initialized. This is inconsistent with
get_vector()/get_metadata(), which raise NotImplementedError when a non-inmemory backend can’t
fulfill the request.
Code

semantica/vector_store/vector_store.py[R845-848]

+                "implement count. Vector counting is only supported for the "
+                "inmemory backend."
+            )
+        return 0
Evidence
The new count() implementation explicitly returns 0 when _backend_store is missing, while other
backend-agnostic accessors raise when a non-inmemory backend cannot serve the request; VectorManager
now depends on count() for integrity/stats, so this can mask backend initialization problems.

semantica/vector_store/vector_store.py[809-826]
semantica/vector_store/vector_store.py[827-849]
semantica/vector_store/vector_store.py[1484-1511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`VectorStore.count()` falls through to `return 0` when `self.backend != "inmemory"` and `self._backend_store is None`. This can cause `VectorManager.collect_statistics()` and `VectorManager.maintain_store()` to silently misreport a non-inmemory store as empty/healthy when the backend store is actually missing/uninitialized.
### Issue Context
Other public accessors (`get_vector()`, `get_metadata()`) do not silently succeed in this situation; they raise `NotImplementedError`. `count()` should follow the same capability/initialization semantics.
### Fix Focus Areas
- semantica/vector_store/vector_store.py[827-849]
- semantica/vector_store/vector_store.py[809-826]
- semantica/vector_store/vector_store.py[1484-1511]
### Suggested change
- Replace the final `return 0` with an exception for non-inmemory backends when `_backend_store` is missing (e.g., `raise NotImplementedError("Backend store is not initialized; cannot count vectors")` or `RuntimeError`).
- Optionally, keep returning 0 only when `backend == "inmemory"` (already handled) or if you have an explicit, documented meaning for `_backend_store is None`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Unvalidated count callability ✓ Resolved 🐞 Bug ☼ Reliability
Description
VectorStore.count() checks only hasattr(_backend_store, "count") and then calls it; if a backend
adapter exposes a non-callable count attribute/property, this will raise TypeError at runtime.
Defensive validation would make the failure mode a clearer NotImplementedError.
Code

semantica/vector_store/vector_store.py[R840-842]

+        elif self._backend_store is not None:
+            if hasattr(self._backend_store, "count"):
+                return self._backend_store.count()
Evidence
The added code uses hasattr() and immediately invokes .count(), which assumes the attribute is
callable.

semantica/vector_store/vector_store.py[827-847]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`VectorStore.count()` invokes `_backend_store.count()` after only `hasattr()` checking. If `count` exists but isn’t callable (mis-shaped adapter), this becomes a runtime `TypeError`.
### Issue Context
The documented contract is a `count()` method, so this is a low-likelihood defensive improvement; adding a `callable()` check yields a cleaner, capability-style error.
### Fix Focus Areas
- semantica/vector_store/vector_store.py[840-847]
### Suggested change
- Replace the current check with:
- `count_attr = getattr(self._backend_store, "count", None)`
- `if callable(count_attr): return count_attr()`
- otherwise raise `NotImplementedError(...)`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread semantica/vector_store/vector_store.py Outdated
Comment thread semantica/vector_store/vector_store.py Outdated
@Sameer6305

Copy link
Copy Markdown
Collaborator

@yunaremaia can you fix the qodo reviews, before we review it.

yunaremaia added a commit to yunaremaia/semantica that referenced this pull request Aug 11, 2026
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia

Copy link
Copy Markdown
Contributor Author

Fixed both Qodo findings in cd972c9:

  1. Silent zero countVectorStore.count() no longer returns 0 when a persistent backend has no wrapped store. It now raises NotImplementedError, consistent with get_vector()/get_metadata(), so a missing backend initialization can't masquerade as an empty, healthy store.
  2. Unvalidated count callability — the hasattr check was replaced with a getattr + callable() capability check, so a mis-shaped adapter exposes a clean NotImplementedError instead of a runtime TypeError.

Regression tests added for both cases (12/12 passing in the persistent-backend suite). Ready for review.

@Sameer6305

Copy link
Copy Markdown
Collaborator

Fixed both Qodo findings in cd972c9:

  1. Silent zero countVectorStore.count() no longer returns 0 when a persistent backend has no wrapped store. It now raises NotImplementedError, consistent with get_vector()/get_metadata(), so a missing backend initialization can't masquerade as an empty, healthy store.
  2. Unvalidated count callability — the hasattr check was replaced with a getattr + callable() capability check, so a mis-shaped adapter exposes a clean NotImplementedError instead of a runtime TypeError.

Regression tests added for both cases (12/12 passing in the persistent-backend suite). Ready for review.

Thanks @yunaremaia will review it.

yunaremaia added a commit to yunaremaia/semantica that referenced this pull request Aug 11, 2026
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia
yunaremaia force-pushed the fix/855-vectormanager-persistent-count branch from cd972c9 to a5ba75f Compare August 11, 2026 18:11
@yunaremaia

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (was behind) — no conflicts, the 12 persistent-backend tests still pass. Ready for your review whenever convenient.

yunaremaia added a commit to yunaremaia/semantica that referenced this pull request Aug 12, 2026
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia
yunaremaia force-pushed the fix/855-vectormanager-persistent-count branch 2 times, most recently from dc3f967 to 082da9a Compare August 12, 2026 13:11
yunaremaia added a commit to yunaremaia/semantica that referenced this pull request Aug 12, 2026
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia

Copy link
Copy Markdown
Contributor Author

Thanks for assigning #914 to me — I'll take it to merge. Current state: rebased onto latest main (082da9a, no conflicts), the 12 persistent-backend tests pass (VectorManager on FAISS/Qdrant/Pinecone/Milvus + inmemory delegation + NotImplementedError contract), and both Qodo findings are addressed in the earlier commits. Standing by for your review — happy to iterate on anything that comes up.

yunaremaia added a commit to yunaremaia/semantica that referenced this pull request Aug 12, 2026
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia
yunaremaia force-pushed the fix/855-vectormanager-persistent-count branch from 082da9a to 2886e8d Compare August 12, 2026 21:51
…ends (semantica-agi#855)

maintain_store() and collect_statistics() reached into VectorStore
internals (.vectors/.metadata), which only exist for the inmemory
backend — any persistent backend (FAISS, Qdrant, Pinecone, Milvus,
...) crashed with AttributeError.

Add a public backend-agnostic VectorStore.count() accessor following
the get_vector()/get_metadata() precedent (semantica-agi#843) and the
NotImplementedError-on-unsupported-capability precedent of
_filter_by_metadata() (semantica-agi#848): inmemory counts its dict, persistent
backends delegate to count() when available, and raise
NotImplementedError otherwise. VectorManager methods now go through
count(); maintain_store() keeps the exact inmemory semantics (separate
vector/metadata dict counts) and reports a 1:1 count for persistent
backends, where metadata is stored alongside each vector.

Tests: 10 hermetic unit tests covering inmemory, delegation and the
NotImplementedError path. Core vector_store suite: 40 passed.
Address Qodo review findings on semantica-agi#914:
- Persistent backend with no wrapped store no longer silently returns 0
  (which masked a missing initialization as an empty, healthy store);
  it now raises NotImplementedError like get_vector()/get_metadata().
- A mis-shaped adapter exposing a non-callable 'count' attribute now
  surfaces a clean NotImplementedError instead of a TypeError, via a
  getattr + callable() capability check.

Adds regression tests for both cases.
@yunaremaia
yunaremaia force-pushed the fix/855-vectormanager-persistent-count branch from 2886e8d to 3838a4c Compare August 13, 2026 13:24
semantica-agi#914)

- FAISSStore.count(): returns len(index.vector_ids); 0 when no index exists yet
- SQLiteVecStore.count(): delegates to get_stats()[vector_count] (SELECT COUNT(*))
- PgVectorStore.count(): delegates to get_stats()[vector_count] (SELECT COUNT(*))
- VectorStore.count(): fix misleading NotImplementedError message; now describes
  how to add count() support to a backend adapter rather than claiming only the
  inmemory backend can ever support counting
- VectorManager.maintain_store(): split inmemory and persistent paths:
  * inmemory: independently reads len(vectors) and len(metadata) and compares
    them as an integrity check (original semantics preserved)
  * persistent: calls store.count(); returns metadata_count=None because
    metadata is co-located with vectors in the backend and cannot be counted
    independently; never manufactures metadata_count=vector_count as a vacuous
    tautology (semantica-agi#914 Qodo review)
- Tests: rewrite test_vector_manager_persistent.py with 31 tests covering
  dispatch logic, inmemory divergence detection, persistent metadata_count=None
  invariant, FAISSStore/PgVectorStore via mocks, and SQLiteVecStore via real
  in-memory SQLite (skipped when sqlite-vec absent)
@Sameer6305
Sameer6305 self-requested a review August 13, 2026 15:36
Sameer6305
Sameer6305 previously approved these changes Aug 13, 2026

@Sameer6305 Sameer6305 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yunaremaia for the solid work on this fix and for addressing the persistent-backend compatibility issue.

I reviewed the implementation against the existing VectorStore/VectorManager abstractions, backend behavior, edge cases, and the added tests.

During review, I found that the initial count() abstraction still left the shipped persistent backends without an actual count implementation, and maintain_store() was treating metadata_count == vector_count as a guaranteed invariant for persistent stores. So corrected this by:

  • Adding count() implementations for FAISS, SQLiteVec, and PgVector where reliable counts are available.
  • Keeping NotImplementedError for backends where a reliable count cannot currently be guaranteed.
  • Updating maintain_store() so persistent backends report metadata_count=None rather than manufacturing a 1:1 metadata invariant.
  • Improving the unsupported-count error message.
  • Expanding the regression tests to cover the new backend behavior and persistent/in-memory semantics.

These fixes are included in commit df02fa88 (fix(vector_store): implement count() on FAISS/SQLite/PgVector backends (#914)).

The focused validation passed (25/25 targeted tests; SQLite-specific tests are skipped when sqlite-vec is unavailable), and the working tree/diff checks are clean.

@KaifAhmad1 From my side, this is approved and ready for your final review/verdict before merge.

…emantica-agi#914, closes semantica-agi#855)

Records the VectorStore.count() accessor, the FAISS/SQLite/PgVector
implementations added during review, and the maintain_store()
metadata_count fix (no longer fabricates equality for persistent backends).

@KaifAhmad1 KaifAhmad1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — thanks both.

@yunaremaia, nice fix for #855: the backend-agnostic VectorStore.count() is the right shape (following the get_vector()/get_metadata() precedent from #843), and you were quick to close out both Qodo findings.

@Sameer6305, good catch that the initial version didn't actually ship a count() on any real backend, and that maintain_store() was manufacturing a vacuous metadata_count == vector_count for persistent stores — the metadata_count: None fix is the honest answer there.

Independently reviewed the diff against the actual FAISSStore/PgVectorStore/SQLiteVecStore internals (not just the description) — confirmed FAISS really has no delete path so len(index.vector_ids) is safe, confirmed FAISS's count() correctly avoids the get_stats() "no_index" dict (which lacks a vector_count key, unlike Pg/SQLite's), and checked there are no other callers in the repo that depend on metadata_count being a non-None int. Added a changelog entry (859690c) documenting the fix. No blockers — ready to merge.

@yunaremaia

Copy link
Copy Markdown
Contributor Author

Thank you @KaifAhmad1! The backend-agnostic count() shape was the goal — glad it matches the repo's precedent. Standing by for the CI to finish.

@KaifAhmad1
KaifAhmad1 merged commit 43bac61 into semantica-agi:main Aug 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VectorManager.maintain_store() and collect_statistics() crash with AttributeError on persistent VectorStore backends

3 participants