Skip to content

symfonic.memory.backends.in_memory_deletion

in_memory_deletion

Atomic in-process deletion of a scope, its edges, and workset progress.

cascade_incident_edges

cascade_incident_edges(store: Any, scope: TenantScope, node: Any) -> None

Erase native incident descendants of the actual deleted owner.

A descendant-owned edge may point to an ancestor node. Keeping that edge when its ancestor endpoint is deleted would resurrect it if the ID is re-added. Legacy callers retain their exact ancestor-chain cascade.

Source code in src/symfonic/memory/backends/in_memory_deletion.py
def cascade_incident_edges(store: Any, scope: TenantScope, node: Any) -> None:
    """Erase native incident descendants of the actual deleted owner.

    A descendant-owned edge may point to an ancestor node. Keeping that edge
    when its ancestor endpoint is deleted would resurrect it if the ID is
    re-added. Legacy callers retain their exact ancestor-chain cascade.
    """
    edges = store._edges.get(scope.tenant_id, {})
    native = _is_native_scope(scope)
    prefixes = scope.ancestor_prefix_paths()
    root = stored_scope_path(node.properties, node.tenant_id, scope=scope)
    doomed = []
    for identifier, edge in edges.items():
        if str(node.id) not in (str(edge.source), str(edge.target)):
            continue
        owner = getattr(edge, "_scope_path", None)
        if native:
            owner = stored_scope_path({"scope_path": owner}, edge.tenant_id, scope=scope)
        if owner in prefixes or (
            native and owner is not None and root is not None and is_in_subtree(owner, root)
        ):
            doomed.append(identifier)
    for identifier in doomed:
        del edges[identifier]

delete_node

delete_node(store: Any, scope: TenantScope, node_id: Any, cascade: bool) -> None

Preserve visible deletion semantics under the backend writer lock.

Source code in src/symfonic/memory/backends/in_memory_deletion.py
def delete_node(store: Any, scope: TenantScope, node_id: Any, cascade: bool) -> None:
    """Preserve visible deletion semantics under the backend writer lock."""
    store._erase_epochs[scope.tenant_id] += 1
    tenant_nodes = store._nodes.get(scope.tenant_id, {})
    node = tenant_nodes.get(str(node_id))
    if node is None or not store._node_visible(scope, node):
        return
    tenant_nodes.pop(str(node_id), None)
    if cascade:
        cascade_incident_edges(store, scope, node)

delete_subtree

delete_subtree(store: Any, scope: TenantScope) -> int

Erase every node at scope or below it, with its edges.

Atomic by construction: the whole sweep runs in one synchronous span with no await in it, so no concurrent write on this loop can land in a scope between its discovery and its deletion.

Source code in src/symfonic/memory/backends/in_memory_deletion.py
def delete_subtree(store: Any, scope: TenantScope) -> int:
    """Erase every node at ``scope`` or below it, with its edges.

    Atomic by construction: the whole sweep runs in one synchronous span
    with no ``await`` in it, so no concurrent write on this loop can land
    in a scope between its discovery and its deletion.
    """
    from symfonic.memory.backends.in_memory_ownership import native_owner
    store._erase_epochs[scope.tenant_id] += 1  # a request, even one that finds nothing
    root = materialise_scope_path(scope)
    native = _is_native_scope(scope)

    def swept(properties, tenant):
        path = stored_scope_path(properties, tenant, scope=scope)
        return path is not None and is_in_subtree(path, root)
    tenant_nodes = store._nodes.get(scope.tenant_id, {})
    doomed = [
        node_id
        for node_id, node in tenant_nodes.items()
        if swept(node.properties, node.tenant_id)
    ]
    tenant_edges = store._edges.get(scope.tenant_id, {})
    doomed_set = set(doomed)
    for edge_id in [
        eid
        for eid, edge in tenant_edges.items()
        if (swept({"scope_path": getattr(edge, "_scope_path", None)}, edge.tenant_id)
            if native else not native_owner(getattr(edge, "_scope_path", None)) and (
                str(edge.source) in doomed_set or str(edge.target) in doomed_set))
    ]:
        del tenant_edges[edge_id]
    for node_id in doomed:
        del tenant_nodes[node_id]
    if native:
        # A positional migration must not move a v0 row back under it.
        store._erased_subtrees[scope.tenant_id].add(root)
    for key in tuple(store._workset_cursors):
        if is_in_subtree(key[0], root):
            del store._workset_cursors[key]
    return len(doomed)