fix(surrealdb): cascade synapses brain-agnostically on every neuron-delete path#89
Open
RobertSigmundsson wants to merge 3 commits into
Conversation
added 3 commits
July 26, 2026 01:40
…udit DB-01) delete_neuron deleted connected synapses with a 'brain_id = <brain> AND in/out = neuron' filter. Some write paths create synapses with a NULL brain_id, which that predicate silently skipped — orphaning them when the neuron was pruned. The schema's cascade_delete_synapses event does not catch these either: it does not fire on the SDK conn.delete() call. Match the neuron endpoint alone (still two index-friendly single-field DELETEs); a synapse pointing at a deleted neuron must go regardless of brain. Root-caused from live orphan synapses (7, all brain_id=NULL) found by the check-graph-integrity regression guard. Adds a mock-based unit test asserting the synapse-delete predicate is brain-agnostic + the delete error path. Tests: 2 new pass; 34 related unit tests pass; ruff clean. (cherry picked from commit 144c39d)
… shape (audit DB-01) 8e53b1f made delete_neuron cascade brain-agnostically (DELETE synapse WHERE in/out = neuron:<id>, no brain_id filter) so NULL-brain_id orphan synapses are caught, but did not update the pre-existing upstream test test_delete_neuron_cascade_uses_in_out, which still asserted the old "brain_id = '<brain>' AND in = neuron:<id>" shape. That test was already red on rebase/2.10.5 (prod); the rebase onto v2.12.1 inherited it faithfully. Update the assertions to the endpoint-only shape and add positive brain-agnostic checks (no brain_id in either DELETE) to lock in the audit DB-01 intent — otherwise a future change re-adding a brain_id filter would silently reintroduce the NULL-brain_id orphan-synapse leak. (cherry picked from commit 2a43020)
…dit DB-01 re-accrual) The 07-19 DB-01 fix (144c39d) made delete_neuron() cascade-delete synapses brain-agnostically, but cleanup_ephemeral_neurons() (smem_auto's session-end ephemeral sweep) had its own raw conn.delete() with no cascade at all, orphaning both synapses and neuron_state rows every time it ran. Route it through delete_neuron() instead of duplicating the delete logic. (cherry picked from commit dff01dd63af5ddac24e315c346516573b53553e2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
delete_neurondeletes the neuron's synapses with abrain_id-filtered predicate:Some write paths create synapses with a NULL
brain_id. Those rows do not match the filter, sothey survive the neuron they point at and become dangling orphans. The schema's
cascade_delete_synapsesevent does not save us either — it does not fire on the SDKconn.delete()call used for the neuron record itself.
Change
Drop the
brain_idterm from the cascade. Matching the neuron endpoint alone is the correctpredicate: a synapse whose
in/outpoints at a deleted neuron is dangling regardless of its ownbrain_id.Second delete path with the same leak
cleanup_ephemeral_neurons()— the session-end ephemeral sweep behindsmem_auto— did not gothrough
delete_neuron()at all. It had its own rawconn.delete(f"neuron:{nid}"), so it skipped boththe synapse cascade and the
neuron_statecleanup, orphaning rows on every run. Fixingdelete_neuronalone would have left this path leaking, so the third commit routes it throughdelete_neuron()instead of duplicating the delete logic:Why upstream benefits
Fixes a real orphan leak with no downside: the predicate is narrower in what it can wrongly keep and
identical in what it deletes for correctly-tagged rows. It also preserves the performance property that
made the two-statement form necessary in the first place — each single-field
DELETEhits its ownidx_synapse_in/idx_synapse_outindex (~5 ms total), whereas a combinedORacross the two fieldsfalls back to a full scan in SurrealDB 3.2.0's planner (~1.2 s), which was the dominant cost behind
prune timeouts on large brains. That measurement comment is retained in the code.
Evidence
origin/mainimport surreal_memoryruff checkruff format --checkmypy src/ --ignore-missing-importspytest -m "not stress" -n autoNew files:
tests/unit/test_delete_neuron_cascade.py(+81) andtests/unit/test_cleanup_ephemeral_neurons_cascade.py(+75);tests/unit/test_surrealdb_synapse_queries.pyupdated to the brain-agnostic query shape.Risks / notes for the reviewer
deletes it. That situation is already inconsistent data — a cross-brain edge — but a reviewer who knows
of a valid use for it should say so.
cleanup_ephemeral_neuronsnow performs a fulldelete_neuronper row instead of a raw delete. That isstrictly more work per ephemeral neuron — correct, but a reviewer running very large ephemeral sweeps
may want to confirm the cost is acceptable.