Both methods are concrete on ``BaseCollection`` (``facet_counts`` raises
``UnsupportedCapabilityError``; ``get_all_metadata`` pages through
``self.get(include=["metadatas"])``). Python MRO resolves them on
``EmbeddingCollection`` before ``__getattr__`` ever fires, so without an
explicit forwarder the wrapper silently runs the base default instead of
delegating to the wrapped backend's optimized implementation. The pattern
matches the existing explicit forwarders for ``distance_metric``,
``lexical_search``, and the embedder-identity trio — all added to fix the
same shadow.
What this means in production for the three backends that get wrapped
(``EmbeddingCollection`` only applies to ``requires_explicit_embeddings``
backends — qdrant, pgvector, sqlite_exact; chroma is unwrapped and
unaffected):
- **facet_counts shadow (#1868 regression)**: every ``mempalace_status``,
``list_wings``, ``list_rooms``, ``get_taxonomy`` call routes through
the gated ``col.facet_counts(...)`` path. The capability check passes
(``supports_metadata_facets`` is on the backend), but the call hits the
wrapper's MRO-resolved ``BaseCollection.facet_counts`` and raises
``UnsupportedCapabilityError``. ``mcp_server``'s broad ``except`` swallows
it, logs ``WARN Failed to fetch metadata facets, falling back to client-
side loop: backend does not support facet_counts``, and counts via the
O(n) Python loop — the exact behavior #1868 was designed to eliminate.
- **get_all_metadata shadow (#1796 / #1892 regression)**: the BaseCollection
default pages through ``self.get(include=["metadatas"])`` — fine for
Chroma's SQL OFFSET cursor, but on wrapped backends (qdrant, pgvector)
the inner's overridden ``get_all_metadata`` is unreachable. For pgvector
specifically, this means #1892's ``with_document=False`` fast path is
never taken even though it's implemented — every metadata-only fetch
transfers the full document column over the wire. On a 13k-drawer remote
pgvector palace over WAN that's ~13MB per call, dominating wall time.
Why no test caught it: backend tests (``test_qdrant_backend.py``,
``test_pgvector_backend.py``) call the methods directly on the raw
collection, not through the wrapper. ``test_mcp_server.py`` facet tests
use ``MagicMock()`` for the collection, which synthesizes attributes on
demand and bypasses MRO entirely. Neither path covers the seam where the
bug lives: ``palace.get_collection() -> EmbeddingCollection -> .method()``.
Three tests pin both the fix and the bug class:
- ``test_facet_counts_forwards_to_inner`` — direct integration through the
wrapper, asserts the inner's recorded call matches.
- ``test_get_all_metadata_forwards_to_inner`` — same shape, plus a sentinel
``get()`` on the inner so a missing forwarder would route to the base
default and pick up the wrong data (observable failure, not silent).
- ``test_wrapper_forwards_all_concrete_basecollection_methods`` — meta-test
that enumerates every concrete public method on ``BaseCollection`` via
``inspect.getmembers`` and asserts each one is explicitly defined on
``EmbeddingCollection``. Catches the bug *class*: any future
``BaseCollection`` method with a concrete default body becomes a CI
failure the moment it's added without a wrapper forwarder, with a message
pointing straight at the file to edit.
Full env-cleared suite: 3205 passed, 20 skipped. ``ruff check`` and
``ruff format --check`` both clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC5Qsknh2zFRtRvVyjXiTA
Address #1707 review:
- _as_list also wraps a bare dict (single metadata) — list({'k':1}) -> ['k']
would drop the values — and returns list inputs as-is (no copy, Copilot
perf note); other iterables are materialized once.
- Normalize ids and metadatas (not just documents) in add/upsert/update so
a scalar id/metadata stays length-aligned with documents/embeddings.
- Widen query_texts annotation to list[str] | str to match the behavior.
- Tests: bare-str ids + dict metadatas, dict wrapping, list-returned-as-is.
EmbeddingCollection did _embed_texts(list(documents)). For ChromaDB's
OneOrMany shape, a bare str document splits into per-character 'docs'
(list("abc") -> ['a','b','c']), embedding each character and breaking
length alignment with ids/metadatas on explicit-vector backends
(pgvector, sqlite_exact). Normalize str -> [str] via _as_list() at all
four sites (add/upsert/update documents, query query_texts) and pass the
normalized list to the inner backend too. Addresses PR #1706 review
(Gemini + Copilot, HIGH).