symfonic.memory.backends.postgres_graph¶
postgres_graph ¶
PostgreSQL graph backend using JSONB storage.
Implements GraphBackend protocol with asyncpg for all database I/O.
All operations enforce tenant isolation via WHERE tenant_id = $N.
asyncpg must be installed.
pip install symfonic-core[postgres]
PostgresGraphBackend ¶
GraphBackend backed by PostgreSQL with JSONB storage.
All operations enforce tenant isolation via WHERE tenant_id = $N.
Source code in symfonic/memory/backends/postgres_graph.py
pool
property
¶
The pool this backend speaks through, and so its transaction domain.
Public because the consolidation commit has to prove that the graph, the lease and the staged records share one domain before it will call a cycle atomic. A proof that read a private attribute would be a proof about this class's spelling.
backfill_scope_paths
async
¶
Eagerly backfill NULL scope_path rows (design §6.b migration).
Pre-v8.0 rows have scope_path IS NULL; they read as a 1-level root
path tenant\x1f<tenant_id> via the query-time dual-read, so this
eager backfill is OPTIONAL (online-safe, idempotent) — it just
materialises the same value into the column so the B-tree index and
the scorer's dual-read agree. Returns the number of rows updated.
Uses a parameterised expression so adopter tenant_ids cannot inject.
Source code in symfonic/memory/backends/postgres_graph.py
delete_subtree
async
¶
Erase scope and every descendant scope in one statement.
A data-modifying CTE, not two round trips and not a client-side scan: one statement is one implicit transaction, so a write that lands in a descendant scope while this runs is either wholly before the sweep's snapshot (and erased) or wholly after it. The enumerate-then-delete shape has a window between those two facts, and the operation that opens it is a tenant deletion running while other work drains.
The predicate is the delimiter-terminated descendant sweep from
:mod:symfonic.memory.subtree — the one place a LIKE on a scope
path is correct, and only because the delimiter is appended. Without
it, forgetting acme would take acmecorp with it.
Edges go if either endpoint went, or if the edge itself sits in the subtree; the same two placeholders serve both halves.
Source code in symfonic/memory/backends/postgres_graph.py
ensure_schema
async
¶
Create graph tables (nodes + edges) and their indexes.
Does NOT create the memory_vectors table — that is the
responsibility of :class:PostgresVectorBackend.ensure_schema.
Source code in symfonic/memory/backends/postgres_graph.py
query_edges
async
¶
query_edges(scope: TenantScope, filters: dict[str, Any] | None = None, limit: int = 50, offset: int = 0) -> list[MemoryEdge]
Query edges directly from memory_edges table with pagination.
Supports optional relationship filter. All other keys in filters
are ignored to avoid SQL injection risk.
Source code in symfonic/memory/backends/postgres_graph.py
query_subtree
async
¶
query_subtree(scope: TenantScope, filters: dict[str, Any], limit: int | None = None) -> list[MemoryNode]
Query nodes at scope or below it (the descendant read).
Identical to :meth:query_nodes but for the scope predicate: the
delimiter-terminated descendant sweep in place of the ancestor
exact-IN. Sharing the body is deliberate — the two reads must agree
about layer, label, ordering and truncation, and the only thing that
may differ between them is which direction of the hierarchy they mean.
Source code in symfonic/memory/backends/postgres_graph.py
query_subtree_page
async
¶
query_subtree_page(scope: TenantScope, filters: dict[str, Any], *, limit: int, offset: int) -> list[MemoryNode]
One native descendant page without reading every prior page.
GraphAdminService accepts offsets for its browser contract. Its
compatibility fallback must fetch through offset + limit because
the historical backend protocol has no offset; Postgres does, and at
volume that fallback turns page fifty into a ten-thousand-row process
allocation. Keep the optional seam on the concrete adapter until the
backend protocol can add it without breaking adopters.
Source code in symfonic/memory/backends/postgres_graph.py
traverse
async
¶
BFS graph traversal up to max_depth hops from start node.
Source code in symfonic/memory/backends/postgres_graph.py
upsert_edge
async
¶
Insert edge or increment weight by 1 if the same edge already exists.
Uniqueness is determined by (tenant_id, source, target, relationship).
The unique index idx_memory_edges_upsert must exist for ON CONFLICT
to resolve correctly (created by ensure_schema).
Source code in symfonic/memory/backends/postgres_graph.py
matches_extra ¶
Apply arbitrary property filters not handled by SQL.
Source code in symfonic/memory/backends/postgres_graph.py
row_to_node ¶
Convert an asyncpg Record to a MemoryNode.
v6.2 T02 note: spreading_access_count has no dedicated column
(no schema migration was required by the v6.2 carve-out). It is
round-tripped through the properties JSONB under the reserved
key __spreading_access_count; on read we lift the field out of
properties so the Python model remains clean, and on write
_node_properties_jsonb serialises it back in.
v7.2.1 hotfix: the embedding pgvector column is now hydrated
onto MemoryNode.embedding. Prior versions silently dropped the
column on read, which collapsed RetrievalScorer.semantic_similarity
(weight 0.4) to zero for every Postgres-loaded graph candidate.