Add optional PostgreSQL backend with pg_sorted_heap support#665
Add optional PostgreSQL backend with pg_sorted_heap support#665skuznetsov wants to merge 3 commits into
Conversation
web3guru888
left a comment
There was a problem hiding this comment.
PostgreSQL Backend Review — Thorough Read-Through
Very solid PR. This is a well-architected backend addition that I've been hoping to see since #413 laid the seam. I read every line of postgres.py (631 LOC), the config changes, the mcp_server refactor, palace.py routing, tests, docs, and the install script. Here's what I found:
Strengths
1. Batched writes via INSERT ... SELECT ... FROM unnest()
This is the right approach for PostgreSQL — avoids N round-trips and the delete-then-add antipattern that ChromaDB's update() effectively does. The ON CONFLICT (id) DO UPDATE SET ... upsert is clean and atomic. The local dedup of rows_by_id before hitting the DB is a nice touch that prevents wasted work on duplicate IDs within a single batch.
2. Fail-closed metadata filter handling
_where_to_sql() raising ValueError on unsupported operators ($gt, $lt, $gte, $lte) rather than silently ignoring them is exactly right. ChromaDB supports these, but silent degradation in a database backend would cause subtle retrieval bugs. The explicit error forces callers to adapt.
3. Lazy vector index with catalog estimates
Using pg_stat_all_tables.n_live_tup / pg_class.reltuples instead of COUNT(*) for the 5,000-row threshold check is smart — avoids a sequential scan on every write batch. The _rows_since_index_check interval limiter prevents even the catalog query from running on every insert. And the _local_row_estimate fallback handles freshly-created tables where ANALYZE hasn't run yet.
4. mcp_server.py refactor is clean
Replacing the direct chromadb.PersistentClient import with palace.get_collection() is the right abstraction. The cache invalidation via _get_collection_cache_key() that composes (backend, target, collection_name, inode) handles both backends correctly. The _fetch_all_metadata loop change from while offset < total to while True with break-on-empty is safer since count() could race with concurrent writes.
5. estimated_count() on BaseCollection
Good addition — defaults to count() so ChromaDB callers are unaffected, but PostgreSQL can use catalog stats. The layers.py change to prefer estimated_count when available is a nice performance win for status checks.
6. Test coverage is thorough
315 lines of tests covering: input validation, where-clause parsing (including $or, $and, $in, $nin, $eq, $ne), empty-id rejection, batch conflict handling, dedup within a batch, lazy index creation, estimated count, and the backend factory's create/no-create paths. All without requiring a live PostgreSQL server — good use of monkeypatching.
Minor observations (not blocking)
1. _metadata_value() stringifies everything — metadata values go through str() before comparison, which means where={"count": 5} compares the string "5" against metadata->>'count'. This matches ChromaDB's behavior (which also stringifies in its SQLite layer), but it's worth a doc comment since PostgreSQL's jsonb could support typed comparisons. Could be a future enhancement.
2. The _embed() function uses a module-global _embedder — this is fine for single-process use, but if someone embeds MemPalace in a multi-threaded server, SentenceTransformer.encode() may not be thread-safe depending on the model backend. A threading.Lock around the lazy init would be defensive. Low priority since the typical deployment is single-process CLI.
3. Connection lifecycle — autocommit = True is the right choice for this use case (no long transactions), but if the connection drops mid-session (network blip to a remote PG), the next _get_conn() call will reconnect since it checks self._conn.closed. Good. One edge case: if psycopg2 raises OperationalError mid-query, the connection may be left in a bad state without closed being set. A try/except around cursor operations that resets self._conn = None on OperationalError would make it more resilient for long-lived server deployments.
4. sorted_heap table has PRIMARY KEY (wing, room, id) but also a unique index on (id) — this means id is globally unique across wings/rooms, which matches ChromaDB semantics. The composite PK gives sorted_heap the sort key it needs. Clean.
Impact for downstream consumers
This is significant for anyone running MemPalace in multi-agent or server environments. The ChromaDB backend works well for single-user CLI, but concurrent access from multiple agents (e.g., specialist agents each writing to the palace simultaneously) hits ChromaDB's SQLite locking. PostgreSQL with proper connection pooling would eliminate that bottleneck. The BaseCollection abstraction also opens the door for future backends (LanceDB per #574, etc.).
APPROVE — this is ready to merge. Clean architecture, solid test coverage, thorough docs. The minor observations above are suggestions for follow-up, not blockers.
Formalizes the BaseCollection/BaseBackend contract introduced as a seam in #413 into an interchangeability spec that third-party backends can build to. Driven by six in-flight backend PRs (#574, #643, #665, #697, #700, #381) each implementing the interface differently. Key decisions captured: entry-point distribution, typed QueryResult/ GetResult replacing Chroma dict shape, daemon-first multi-palace model via PalaceRef, required where-clause subset (incl. $contains), mandatory embedder injection with model-identity validation, capability tokens, shared pytest conformance suite, and a backend-neutral migrate/verify CLI.
Incorporates review feedback from skuznetsov (Postgres, #665) and dekoza (Lance, #574) on issue #737: - §1.5: split 'accepts embeddings=' (signature compliance) from 'persists embeddings as-is' (correctness). Adds supports_embeddings_passthrough capability; the former is universal, the latter is required to label a migration lossless. - §1.5: model identity check becomes a three-state machine (known_match / known_mismatch / unknown) so legacy palaces without recorded identity don't hard-fail on upgrade. - §1.4: makes explicit that supports_contains_fast is the ONLY performance floor the spec promises; without it callers MUST assume O(n). $contains is a correctness requirement, not a performance one. - §3.3: clarifies auto-detect is an upgrade-compat path only, never the selection mechanism for new palaces. - §8.2: migrate CLI refuses to run against a target lacking supports_embeddings_passthrough unless --accept-re-embed is passed; migration record now captures lossless status and model identities.
|
(Updated version of my review comment above — adding a question for @web3guru888) Concern: pg_sorted_heap as "preferred" path introduces a single-maintainer dependency Thanks for the solid work on the backend abstraction — the lazy HNSW index creation and extension auto-detection are well thought out. I want to flag a concern about the dual-path architecture that I do not think was addressed in @web3guru888's otherwise thorough review. The "preferred" backend (
@web3guru888 — your review praised the composite PK design on the Suggestion: Ship this PR with pgvector as the only path. If This would cut ~200 lines of dual-path code, eliminate the install script, and remove a bus-factor-of-one dependency from the project. |
|
Thanks for raising this. I agree the availability / bus-factor concern is real in the general case, and I do not want this PR to make I pushed 5976c2f to tighten the docs framing:
Update: the PGXN point was fair when raised. I do not think removing the So the intended boundary is: no new hard dependency for MemPalace, no managed-service requirement to use |
…nd registry (RFC 001 §10) Advances RFC 001 §10 cleanup so backend-author PRs (#574 LanceDB, #665 Postgres, #700 Qdrant, #697 hosted, #643 PalaceStore, #381 Qdrant) have a stable target to align against. Scope (this PR): - Typed QueryResult / GetResult dataclasses replace Chroma's dict shape at the BaseCollection boundary (§1.3). A transitional _DictCompatMixin keeps existing callers working while the attribute-access migration proceeds. - BaseCollection is now kwargs-only across add/upsert/query/get/delete/update with ABC defaults for estimated_count/close/health and a non-atomic default update() (§1.1–1.2). - PalaceRef replaces raw path strings at the backend boundary (§2.2). - BaseBackend ABC with get_collection/close_palace/close/health/detect (§2.3). - mempalace.backends entry-point group + in-tree registry with resolve_backend_for_palace priority order matching §3.2–3.3. - ChromaCollection normalizes chroma returns into typed results; unknown where-clause operators raise UnsupportedFilterError (no silent drop, §1.4). - ChromaBackend absorbs the inode/mtime client-cache freshness check previously duplicated in mcp_server._get_client() (§10 + PR #757). - searcher.py migrated to typed-attribute access as the reference call site; remaining callers land in a follow-up. - pyproject: chroma registered via [project.entry-points."mempalace.backends"]. Out of scope (explicit follow-ups): - Full caller migration off the dict-compat shim across palace.py, mcp_server.py, miner.py, convo_miner.py, dedup.py, repair.py, exporter.py, palace_graph.py, cli.py, closet_llm.py. - Embedder injection + three-state EmbedderIdentityMismatchError check (§1.5). - maintenance_state() / run_maintenance() benchmark hooks (§7.3). - AbstractBackendContractSuite full coverage (§7.1–7.2). - mempalace migrate / mempalace verify CLI rewrites through BaseCollection (§8). Tests: 970 passed (up from 967 on develop); new coverage for typed results, empty-result outer-shape preservation, \$regex rejection, registry lookup, priority resolver, and PalaceRef-kwarg ChromaBackend.get_collection. Refs: #743 (RFC 001), #989 (RFC 002 tracking issue).
21df7a8 to
16f8af7
Compare
Rebuild the stage 2 PostgreSQL backend on top of the RFC 001 backend contract merged in MemPalace#995 instead of carrying forward the stale Chroma-shaped adapter. The new backend implements BaseBackend/BaseCollection with PalaceRef, typed QueryResult/GetResult returns, registry discovery, and env/config backend selection while keeping Chroma as the default path. The PostgreSQL collection supports both pg_sorted_heap and pgvector. It prefers pg_sorted_heap when the extension is installed, falls back to pgvector when only vector is installed, writes rows with INSERT ... SELECT FROM unnest(...) and ON CONFLICT, preserves first-wins semantics for add() and last-wins semantics for upsert(), supports required metadata filters, returns optional embeddings, keeps exact count() separate from estimated_count(), and creates vector indexes lazily after the threshold. Text embedding reuses Chroma's default local embedding function so the postgres extra only adds psycopg2-binary instead of a second ML dependency stack. Add user-facing PostgreSQL backend documentation, a PGXS install helper for pg_sorted_heap/pgvector, README links, lockfile metadata for the postgres extra, and unit coverage for registry exposure, config/env routing, PostgreSQL validation, filter translation, batch upsert, typed result shapes, estimated count, and palace-level backend selection. Verification: - uv run --frozen python -m pytest tests/ -q --ignore=tests/benchmarks -> 1044 passed, 1 warning - uv run --frozen python -m pytest tests/test_backends.py tests/test_config.py tests/test_config_extra.py -q -> 77 passed - ruff check mempalace/backends/postgres.py mempalace/backends/registry.py mempalace/backends/__init__.py mempalace/config.py mempalace/palace.py tests/test_backends.py tests/test_config_extra.py - ruff format --check mempalace/backends/postgres.py mempalace/backends/registry.py mempalace/backends/__init__.py mempalace/config.py mempalace/palace.py tests/test_backends.py tests/test_config_extra.py - git diff --check && uv lock --check && bash -n scripts/install_pg_backend.sh - uv run --frozen --extra postgres import smoke for psycopg2/PostgresBackend - local PostgreSQL 18 integration smoke with pg_sorted_heap: add/upsert/query/get/delete/health and sorted_heap table AM - local PostgreSQL 18 integration smoke with pgvector fallback: add/query/count and heap table AM - local PostgreSQL 18 forced-threshold vector-index smoke for sorted_hnsw and hnsw - secret-pattern scan across changed files
16f8af7 to
83d5448
Compare
|
Follow-up after #995 / RFC 001 landed: I rewrote this PR on top of the merged backend contract instead of keeping the old Chroma-shaped adapter. The current head is
I also re-ran the hostile review pass after the rewrite. Current verification:
So the review surface should now be the RFC-based backend implementation, not the pre-#995 adapter shape. |
…alace#1072 exist, not a write-from-scratch Previous version claimed Postgres was "out of scope — requires writing a new BaseBackend implementation". That was wrong: @skuznetsov's MemPalace#665 already ships a PostgreSQL backend (pg_sorted_heap preferred, pgvector fallback), and @malakhov-dmitrii's MemPalace#1072 wires MEMPALACE_BACKEND env var through palace._DEFAULT_BACKEND so entry-point backends actually get used. RFC 001 seam (MemPalace#413, MemPalace#995) is merged; registry.py advertises mempalace_postgres as the canonical example. Reframed as parallel track: palace-daemon is deployable today (no migration, wraps current ChromaDB palace); Postgres needs both PRs to land plus a drawer migration (export_palace() + importer — not code we'd write) but eliminates the entire 1.5.x failure class at the storage layer. Starting with palace-daemon since it's deployable now; the daemon is storage-agnostic so Postgres remains available as a later swap. Also fixed palace-daemon lock path: /tmp/palace-daemon-8085.lock (per-port), not /tmp/palace-daemon.lock.
Rebuild the stage 2 PostgreSQL backend on top of the RFC 001 backend contract merged in MemPalace#995 instead of carrying forward the stale Chroma-shaped adapter. The new backend implements BaseBackend/BaseCollection with PalaceRef, typed QueryResult/GetResult returns, registry discovery, and env/config backend selection while keeping Chroma as the default path. The PostgreSQL collection supports both pg_sorted_heap and pgvector. It prefers pg_sorted_heap when the extension is installed, falls back to pgvector when only vector is installed, writes rows with INSERT ... SELECT FROM unnest(...) and ON CONFLICT, preserves first-wins semantics for add() and last-wins semantics for upsert(), supports required metadata filters, returns optional embeddings, keeps exact count() separate from estimated_count(), and creates vector indexes lazily after the threshold. Text embedding reuses Chroma's default local embedding function so the postgres extra only adds psycopg2-binary instead of a second ML dependency stack. Add user-facing PostgreSQL backend documentation, a PGXS install helper for pg_sorted_heap/pgvector, README links, lockfile metadata for the postgres extra, and unit coverage for registry exposure, config/env routing, PostgreSQL validation, filter translation, batch upsert, typed result shapes, estimated count, and palace-level backend selection. Fork resolution notes (Jeffrey Hein, 2026-05-11): - README.md: kept fork README (per feedback_fork_readme_handling). - uv.lock: regenerated from auto-merged pyproject.toml so the fork's existing deps and MemPalace#665's psycopg2-binary extra land coherently. - mempalace/palace.py: took MemPalace#665's get_collection() rewrite + re-added `_DEFAULT_BACKEND = get_backend("chroma")` as a fork-only compatibility shim. mcp_server.py + tests still reference the module attribute (cache invalidation + monkeypatch); follow-up commits will migrate callers to the new abstraction. - tests/test_backends.py: took the union of import sets — fork-side HNSW helpers (_HNSW_MISSING_METADATA_DATA_FLOOR, _pin_hnsw_threads, _segment_appears_healthy, quarantine_invalid_hnsw_metadata, quarantine_stale_hnsw) plus MemPalace#665's new postgres helpers (_metadata_value, _parse_vector_literal, _vec_literal). (cherry picked from commit 83d5448)
…emPalace#665 Cherry-picking MemPalace#665 changed `palace.get_collection()` from `_DEFAULT_BACKEND.get_collection(palace_path, collection_name=...)` to `get_backend(backend_name).get_collection(palace=PalaceRef(...), collection_name=..., options=None)`. `get_configured_collection_name()` survives as a thin back-compat wrapper but the live path now reads `MempalaceConfig().collection_name` directly (env `MEMPALACE_COLLECTION_NAME` or file config). Updated fake_get_collection's signature to match the new contract, switched from monkeypatching the function to setting the env var, and asserts the PalaceRef + options arguments alongside the existing collection_name and create assertions. `palace._DEFAULT_BACKEND` is now aliased to `get_backend("chroma")` (see fork-only shim in palace.py); the monkeypatch on that attribute still routes the captured call because the default backend resolves to chroma. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After cherry-picking MemPalace#665, palace.get_collection() only resolved "use config default" when the caller passed exactly DEFAULT_COLLECTION_NAME. Fork-side callers (searcher.search_memories, convo_miner.process_file, sweeper.sweep_palace, sync.sync_palace, diary_ingest, etc.) pass collection_name=None explicitly per the fork's pre-MemPalace#665 convention, which bypassed the check and propagated None to chromadb. Result: 30 test failures in test_searcher.py with "argument 'name': 'NoneType' object cannot be converted to 'PyString'". Accepting both forms — None (fork) and DEFAULT_COLLECTION_NAME (MemPalace#665) — restores the green floor without disturbing MemPalace#665's structure. Eventual follow-up: migrate fork-side callers to omit the keyword or pass the sentinel, then tighten the type back to `str = DEFAULT_COLLECTION_NAME`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(backends): pgvector substrate via MemPalace#665 cherry-pick + smoke tests
|
Operator data point on the pg_sorted_heap-vs-pgvector decision: the jphein fork cherry-picked this PR onto fork main (commit The fallback codepath works end-to-end:
@dekoza's bus-factor concern (pg_sorted_heap availability) doesn't reproduce on this deployment because we never hit that codepath — pgvector's the only vector type installed; This isn't a vote against Full rationale for the fork's "wait + cherry-pick" composition stance is at |
The plan was written 2026-05-10. Two days later PR #21 (feat/pgvector-age-impl) merged + commit a3ee623 landed the AGE skeleton — together completing all of Phase 0, Phase 1, and Task 2.1. Status header at top of the plan now captures the reconciliation: Phase 0 (preflight) ✅ Done 2026-05-11 Phase 1 (pgvector backend) ✅ Done 2026-05-11 (1.A.1, 1.2, 1.3, 1.4, 1.5) Phase 2.1 (AGE skeleton) ✅ Done 2026-05-11 Phase 2.2 (add_triple) ❌ Not done — canonical next task Phase 2.3 (temporal) ❌ Phase 2.4 (kg_backend cfg) ❌ Phase 3 (migration tool) ❌ Phase 4 (benchmark) ❌ 37 of 92 checkboxes ticked within done-task ranges. 1.B.* (fork-port path) sub-tasks remain unticked because we took the 1.A path (wait for MemPalace#665 + cherry-pick). Evidence for each ✅ entry is in the new status header — file paths, line numbers, and the test/CI fixtures that demonstrate the work. The canonical next task is Phase 2.2: implement add_triple() with Cypher MERGE/CREATE on the existing KnowledgeGraphAGE skeleton. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Operator follow-up to my 2026-05-11 comment above — surfacing a concurrency issue we hit running this code in production. What we observedOn Race
cur.execute("SELECT 1 FROM pg_indexes WHERE indexname = %s", (index_name,))
if cur.fetchone():
self._vector_index_ready = True
return
# ...
cur.execute(
self._sql.SQL("CREATE INDEX {} ON {} USING {} (embedding {})").format(...)
)Under any concurrency setting that allows more than one writer (we run Aggravating factor (separate, lower priority)The existence check is by literal name This second issue is more opinionated to fix (does mempalace own the index name, or accept any compatible HNSW?) — happy to defer that to a design conversation. Minimal hardening that would have prevented the wedgeTwo-line addition; keeps the existing flow: # Inter-connection guard + DDL idempotency
with self._get_conn().cursor() as cur:
cur.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (f"vec_idx:{self.table_name}",))
cur.execute("SELECT 1 FROM pg_indexes WHERE indexname = %s", (index_name,))
if cur.fetchone():
self._vector_index_ready = True
return
# ...
cur.execute(
self._sql.SQL("CREATE INDEX IF NOT EXISTS {} ON {} USING {} (embedding {})").format(...)
)
Local mitigationWe unwedged out-of-band by canceling the duplicate OfferIf a fixup commit on this PR (minimal version above) would help, happy to put one up. Wanted to surface the diagnosis here first since this is your code path and you may want a different shape. |
…ex race surfaced Captures today's two threads: - Phases 4.1 + 4.2 of the pgvector+AGE migration actually ran (271k drawers + KG triples migrated via the substrate-portable out-of-band pipeline since chromadb SIGSEGV blocked the canonical CLI tool). - A pgvector lazy-index race in mempalace's PostgresBackend wedged /mcp for 28+ min on first concurrent-writer load; recovered out-of- band, filed techempower-org/mempalace#73 with two-line hardening proposal, posted operator comment on MemPalace/mempalace#665. - palace-daemon hook.py now os.fork()s + closes all three stdio FDs in the child so a wedged daemon can't block the harness. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…1/4.2 executed Plan previously said Phases 4.1 and 4.2 were "Runbook shipped" and "Documented"; today they actually ran on the homelab palace. Updated the status table to "Executed 2026-05-14" with evidence pointers, and added a post-mortem section covering: - The substrate-portable out-of-band migration pipeline that was needed because chromadb SIGSEGV blocked migrate-to-postgres against the live 271k-drawer source palace. - The pgvector lazy-index race incident: _maybe_create_vector_index has both a lookup-vs-create race and a name-coupled existence check; filed as #73 with a two-line hardening proposal. - The hook detach work in palace-daemon that hardened the harness against any future daemon wedges of this class. - Open follow-ups (await maintainer response on upstream MemPalace#665 before pre-empting their fix; #73 stays pending; mempalace plugin hooks.json auto-update edge case still latent but self-healing on restart). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_maybe_create_vector_index had a SELECT-then-CREATE race plus a name- coupled existence check. Three concurrent writers tipping VECTOR_INDEX_CHECK_INTERVAL_ROWS in the same window each did the same lookup (all saw empty), each issued `CREATE INDEX … USING hnsw` with no IF NOT EXISTS guard, each took ACCESS EXCLUSIVE, and they stacked — wedging mempalace_drawers for ~30 min on 271k drawers and blocking every other write behind them. Operator diagnosis at #73, observed in production on disks 2026-05-14. Hardening: - Wrap the check+create in `pg_advisory_xact_lock(hashtext('vec_idx: <table>'))` so concurrent writers serialize through the lock instead of racing. - The main backend connection is autocommit=True, and psycopg2 forbids toggling autocommit mid-flight (already hit in phase_1_schema). Use a short-lived side connection so the advisory has a real transaction to attach to. - Use `CREATE INDEX IF NOT EXISTS` so any path that slipped past the advisory (e.g. an out-of-band loader pre-creating the index under the expected name) is a no-op, not a duplicate-object error. Out of scope here: `CREATE INDEX CONCURRENTLY` (incompatible with the advisory-lock transaction; pgvector supports it but would need a separate code path), and the structural pg_index-by-column lookup (opens an API/design question about who owns the index name). Upstream parallel: operator comment posted on MemPalace#665 with the same minimal-hardening proposal. Conflict resolution at merge time accepted as the cost of fork-first patching. Refs: #73 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Serialize the lazy vector-index check/create path across PostgreSQL connections with a session-level advisory lock. The backend connection uses autocommit, so a transaction-level advisory lock would not protect the subsequent CREATE INDEX statement; the session lock is released explicitly in a finally block. Make vector-index DDL idempotent with CREATE INDEX IF NOT EXISTS and re-check for an existing index while holding the advisory lock. Also accept a compatible ready/valid HNSW or sorted_hnsw index on the embedding column even if it was created under a different name, which avoids rebuilding a functionally equivalent operator-provided index. Add regression coverage for serialized lazy index creation, compatible existing index detection, and advisory unlock on DDL failure. Verification: uv run pytest tests/test_backends.py -q; uv run pytest tests/ -q --ignore=tests/benchmarks; uv run ruff check mempalace/backends/postgres.py tests/test_backends.py; uv run ruff format --check mempalace/backends/postgres.py tests/test_backends.py; git diff --check. Note: repo-wide ruff format --check still reports pre-existing unrelated formatting drift in 9 tests.
|
Thanks for the production report and the clear lock diagnosis, @jphein. The race is real, and your observation about multiple writers crossing the lazy-index threshold at the same time was the missing operational case. I pushed
Verification I ran locally:
I agree |
Resolve PR MemPalace#665 conflicts after develop advanced. Resolution details: - Keep the RFC backend registry path in palace.get_collection while preserving develop's optional collection_name semantics. - Update MCP reconnect/cache reset to close the selected registry backend instead of the removed palace._DEFAULT_BACKEND singleton. - Combine PostgreSQL backend tests with develop's Chroma HNSW quarantine tests. - Regenerate uv.lock from the merged pyproject so postgres, gpu, dml, and coreml extras are all represented. Verification: uv run pytest tests/test_backends.py tests/test_mcp_server.py::TestCacheInvalidation -q; uv run pytest tests/ -q --ignore=tests/benchmarks; uv run ruff check mempalace/palace.py mempalace/mcp_server.py mempalace/backends/postgres.py tests/test_backends.py tests/test_mcp_server.py; uv run ruff format --check mempalace/palace.py mempalace/mcp_server.py mempalace/backends/postgres.py tests/test_backends.py tests/test_mcp_server.py; uv lock --check; conflict-marker scan on mempalace/tests/uv.lock.
|
Third operator follow-up — substrate cutover is now production-stable on our fork. Posting numbers so they're indexed against this PR while it's still in flight. State
Benchmark results — postgres backend, scale=small, 273k-drawer palace8 of 9 mempalace Headlines:
The "no regression vs chromadb-era heap allocators" was the result I was most worried about given the substrate switch — it survived. Methodology callouts for anyone implementing alternative encoders against this backend
Worth flagging because this also affects anyone implementing the Adjacent work this surfaced
Happy to share probe-level rank data, raw bench JSONs, or the daemon-side |
…37) Four call sites in cli.py still used the legacy positional signature ``backend.get_collection(palace_path, collection_name)`` from before PR #21's MemPalace#665 cherry-pick. They worked because ChromaBackend.get_collection accepts both forms via ``_normalize_get_collection_args`` (the answer to issue #37 question (a): the compat shim exists and is intentional). Migrating for consistency with palace.get_collection and searcher.search_memories, which already use the new shape. Future backends (postgres, in-memory test fakes) may not carry the same positional compat surface; converging the codebase on the kwargs form keeps the migration runway short when one of those tightens. Touched sites (cli.py): - cmd_purge match-count probe (line ~1192) - cmd_drawers list-by-wing/room probe (~1282) - cmd_repair_status drawer count (~1492) - cmd_extract palace open (~1645) One test asserted the old positional call shape — updated to match the new PalaceRef call shape. 75/75 cli tests pass. Closes #37 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…37) (#91) * refactor(cli): migrate get_collection call sites to PalaceRef kwargs (#37) Four call sites in cli.py still used the legacy positional signature ``backend.get_collection(palace_path, collection_name)`` from before PR #21's MemPalace#665 cherry-pick. They worked because ChromaBackend.get_collection accepts both forms via ``_normalize_get_collection_args`` (the answer to issue #37 question (a): the compat shim exists and is intentional). Migrating for consistency with palace.get_collection and searcher.search_memories, which already use the new shape. Future backends (postgres, in-memory test fakes) may not carry the same positional compat surface; converging the codebase on the kwargs form keeps the migration runway short when one of those tightens. Touched sites (cli.py): - cmd_purge match-count probe (line ~1192) - cmd_drawers list-by-wing/room probe (~1282) - cmd_repair_status drawer count (~1492) - cmd_extract palace open (~1645) One test asserted the old positional call shape — updated to match the new PalaceRef call shape. 75/75 cli tests pass. Closes #37 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: ruff format (CI lint pass-through) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
What does this PR do?
Follow-up to #413. This adds an optional PostgreSQL palace backend while keeping ChromaDB as the default zero-config backend.
The PostgreSQL backend supports two extension paths:
pg_sorted_heappreferred path:sorted_heaptable storage,svecembeddings, and lazysorted_hnswvector indexes.pgvectorfallback path: regular heap table,vectorembeddings, and lazyhnswvector indexes when onlyvectoris available.It also adds:
MEMPALACE_BACKEND=postgres/ configbackend: postgresandMEMPALACE_POSTGRES_DSN/ configpostgres_dsn.INSERT ... SELECT ... FROM unnest(...) ... ON CONFLICTwrites for add/upsert, avoiding delete-then-add upserts.COUNT(*)threshold check.pg_sorted_heaporpgvectorfrom source.ChromaDB remains the documented default and the public raw-mode benchmark path. The PostgreSQL backend is opt-in for larger, long-lived, or server/team deployments.
Related: #266
Merge order: This is the Stage 2 PostgreSQL backend follow-up and is intended to merge after the Stage 1 backend seam from #413. #413 is already merged into
develop; this PR builds on that seam rather than replacing it.How to test
uv run python -m pytest tests/ -v617 passed, 106 deselected, 413 warningsuv run pytest -q617 passed, 106 deselected, 413 warningsuv run ruff check .git diff --checkpg_sorted_heap/svecpgvector/vectorINSERT ... SELECT ... FROM unnest(...) ... ON CONFLICTChecklist
python -m pytest tests/ -v)ruff check .)