Skip to content

symfonic.memory.backends.postgres_native_eraser

postgres_native_eraser

Durable native subtree erasure on a separate PostgreSQL principal.

A host composes this role explicitly with PostgresGraphBackend. Without it an oversized forget refuses before mutation. Runtime, writer and publisher remain SELECT-only on jobs; only this role advances them.

start locks lifecycle and graph, erases bounded content inline, or records an invisible subtree job for oversized content once its migration is installed. resume advances bounded chunks on demand; nothing runs in the background.

Every transaction pins the configured schema. Startup attests the eraser login, its grants, the graph relation and separation from the runtime principal. An unverified eraser cannot mutate; drift withdraws admission and a partial erase never reports completion.

EraseStep dataclass

EraseStep(job_id: str | None, complete: bool, discarded: tuple[str, ...] = ())

What one eraser transaction left: complete, or a job still erasing.

EraserAttestation dataclass

EraserAttestation(runtime_login: str, eraser_login: str, binding: Any)

What attest proved, once, on the eraser's own login (CORE-194).

PostgresNativeEraser

PostgresNativeEraser(pool: Any, *, schema: str, receipt_key: bytes, erase_anchor: Any = None, receipt_key_id: str | None = None, receipt_verification_keys: Any = None, anchor_key: bytes | None = None)

Job creation, chunking and completion on the eraser's own login.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
def __init__(
    self,
    pool: Any,
    *,
    schema: str,
    receipt_key: bytes,
    erase_anchor: Any = None,
    receipt_key_id: str | None = None,
    receipt_verification_keys: Any = None,
    anchor_key: bytes | None = None,
) -> None:
    from symfonic.memory.backends.postgres_retirement_receipt import configure

    configure(
        self,
        pool,
        schema,
        receipt_key,
        erase_anchor,
        receipt_key_id,
        receipt_verification_keys,
        anchor_key,
    )
    self._attested: dict[tuple[str, Any], EraserAttestation] = {}

attest async

attest(*, runtime_login: str, binding: Any) -> EraserAttestation

D6 on this login, binding's very graph, and not runtime_login.

Read-only. Refuses with EraserNotReady and withdraws every earlier attestation, so a failed re-attestation cannot leave a stale proof.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def attest(self, *, runtime_login: str, binding: Any) -> EraserAttestation:
    """D6 on this login, ``binding``'s very graph, and not ``runtime_login``.

    Read-only. Refuses with ``EraserNotReady`` and withdraws every earlier
    attestation, so a failed re-attestation cannot leave a stale proof.
    """
    login = await self._probe(runtime_login, binding)
    attestation = EraserAttestation(runtime_login, login, binding)
    self._attested[(runtime_login, binding)] = attestation
    return attestation

attestation_for

attestation_for(runtime_login: str, binding: Any) -> EraserAttestation | None

The standing attestation for that runtime identity, or None.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
def attestation_for(self, runtime_login: str, binding: Any) -> EraserAttestation | None:
    """The standing attestation for that runtime identity, or None."""
    return self._attested.get((runtime_login, binding))

guarding

guarding(runtime_pool: Any) -> PostgresNativeEraser

Guard the runtime's pool with this eraser's anchor proof (CORE-171).

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
def guarding(self, runtime_pool: Any) -> PostgresNativeEraser:
    """Guard the runtime's pool with this eraser's anchor proof (CORE-171)."""
    from symfonic.memory.backends.postgres_erase_incarnation import install

    install(runtime_pool, self._guard)
    return self

require_ready async

require_ready() -> None

Startup: refuse unless this login holds exactly D6 in its schema.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def require_ready(self) -> None:
    """Startup: refuse unless this login holds exactly D6 in its schema."""
    await self._probe(None, None)

resume async

resume(job_id: str) -> EraseStep

Erase one chunk of job_id in the configured schema; the operator's entry.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def resume(self, job_id: str) -> EraseStep:
    """Erase one chunk of ``job_id`` in the configured schema; the operator's entry."""
    if type(job_id) is not str or not job_id:
        raise MemoryContractError("native erase job id must be a nonempty string")
    await self._verified()
    return await self._advance(job_id, None)

retire_tenant async

retire_tenant(scope: Any, *, runtime_login: str, binding: Any) -> EraseStep

Explicit permanent tenant retirement; ordinary start stays reusable.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def retire_tenant(self, scope: Any, *, runtime_login: str, binding: Any) -> EraseStep:
    """Explicit permanent tenant retirement; ordinary ``start`` stays reusable."""
    from symfonic.memory.backends.postgres_erase_retirement import retire

    return await retire(self, scope, runtime_login=runtime_login, binding=binding)

start async

start(scope: Any, *, runtime_login: str, binding: Any, vectors: bool = False) -> EraseStep

Erase content and create the job, or resume the identical one.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def start(
    self, scope: Any, *, runtime_login: str, binding: Any, vectors: bool = False
) -> EraseStep:
    """Erase content and create the job, or resume the identical one."""
    from symfonic.memory.backends.postgres_erase_create import active, create
    from symfonic.memory.backends.postgres_erase_routing import (
        OVERLAPPING,
        overlap_args,
        overlap_sql,
    )
    from symfonic.memory.backends.postgres_native_subtree import GRAPH_LOCK

    await self._verified()
    async with self._transaction(binding) as conn:
        jobs = await self._jobs(conn, runtime_login)
        resumed = await active(conn, jobs, scope)
        if resumed is None:
            await conn.execute(_LIFECYCLE_LOCK, f"symfonic-memory:{scope.tenant_id}")
            await conn.execute(GRAPH_LOCK)
            # A start of this same scope may have committed its job while
            # this one waited; that job is resumed, not an overlap.
            resumed = await active(conn, jobs, scope)
        if resumed is None:
            if await conn.fetch(overlap_sql(jobs), *overlap_args(scope)):
                raise MemoryContractError(OVERLAPPING)
            if vectors:
                from symfonic.memory.backends.postgres_erase_vector_subtree import preflight

                await preflight(conn, jobs, scope)
            created, chunked = await create(conn, jobs, scope, vectors=vectors)
            if not chunked:
                return created
            resumed = created.job_id
        elif vectors:
            from symfonic.memory.backends.postgres_erase_vector_subtree import require_job

            await require_job(conn, jobs, resumed)
    return await self._advance(resumed, binding)

runtime_identity async

runtime_identity(pool: Any) -> tuple[str, Any]

The runtime's session_user and the memory_nodes it resolves.

Source code in src/symfonic/memory/backends/postgres_native_eraser.py
async def runtime_identity(pool: Any) -> tuple[str, Any]:
    """The runtime's ``session_user`` and the ``memory_nodes`` it resolves."""
    from symfonic.memory.backends.postgres_retirement_receipt import runtime_identity as resolve

    return await resolve(pool)