feat(database): update trim_announces_for_aspect to protect favourited destinations and saved contacts from deletion

This commit is contained in:
Ivan
2026-04-22 15:05:40 -05:00
parent 4a34921655
commit 3bbee7eed0
2 changed files with 142 additions and 27 deletions

View File

@@ -50,7 +50,15 @@ class AnnounceDAO:
self.provider.execute(query, params)
def trim_announces_for_aspect(self, aspect, max_rows):
"""Delete oldest rows for this aspect until at most max_rows remain."""
"""Delete oldest rows for this aspect until at most max_rows remain.
Announces that correspond to a favourited destination or to a saved
contact are considered protected and are never deleted by this trim,
even if the total count exceeds ``max_rows``. This prevents purging
of announces (and the path/identity context they provide) for
favourited NomadNet nodes and for messaging contacts when storage
limits are enforced.
"""
if max_rows < 1 or not aspect:
return
row = self.provider.fetchone(
@@ -64,8 +72,19 @@ class AnnounceDAO:
self.provider.execute(
"""
DELETE FROM announces WHERE id IN (
SELECT id FROM announces WHERE aspect = ?
ORDER BY updated_at ASC, id ASC
SELECT a.id FROM announces a
WHERE a.aspect = ?
AND NOT EXISTS (
SELECT 1 FROM favourite_destinations f
WHERE f.destination_hash = a.destination_hash
)
AND NOT EXISTS (
SELECT 1 FROM contacts c
WHERE c.remote_identity_hash = a.identity_hash
OR c.lxmf_address = a.destination_hash
OR c.lxst_address = a.destination_hash
)
ORDER BY a.updated_at ASC, a.id ASC
LIMIT ?
)
""",

View File

@@ -7,12 +7,12 @@ from meshchatx.src.backend.database import Database
from meshchatx.src.backend.database.provider import DatabaseProvider
def _insert(db, dest_hex, aspect, updated_order):
def _insert(db, dest_hex, aspect, updated_order, identity_hex=None):
db.announces.upsert_announce(
{
"destination_hash": dest_hex,
"aspect": aspect,
"identity_hash": "a" * 32,
"identity_hash": identity_hex or ("a" * 32),
"identity_public_key": "cHVibmtleQ==",
"app_data": None,
"rssi": None,
@@ -26,14 +26,37 @@ def _insert(db, dest_hex, aspect, updated_order):
)
def _new_db():
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
db = Database(path)
db.initialize()
return db, path
def _cleanup(db, path):
if db is not None:
try:
db.close()
except Exception:
pass
DatabaseProvider._instance = None
if path:
try:
os.unlink(path)
except OSError:
pass
for suffix in ("-wal", "-shm"):
try:
os.unlink(path + suffix)
except OSError:
pass
def test_trim_announces_for_aspect_drops_oldest():
path = None
db = None
db = path = None
try:
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
db = Database(path)
db.initialize()
db, path = _new_db()
aspect = "lxmf.delivery"
_insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z")
_insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
@@ -43,19 +66,92 @@ def test_trim_announces_for_aspect_drops_oldest():
hashes = {r["destination_hash"] for r in rows}
assert hashes == {"03" * 16, "02" * 16}
finally:
if db is not None:
try:
db.close()
except Exception:
pass
DatabaseProvider._instance = None
if path:
try:
os.unlink(path)
except OSError:
pass
for suffix in ("-wal", "-shm"):
try:
os.unlink(path + suffix)
except OSError:
pass
_cleanup(db, path)
def test_trim_preserves_favourited_destination():
"""Favourited NomadNet/destination announces must survive aspect trim."""
db = path = None
try:
db, path = _new_db()
aspect = "nomadnetwork.node"
_insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z")
_insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
_insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
_insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
db.announces.upsert_favourite("01" * 16, "Favourite Node", aspect)
db.announces.trim_announces_for_aspect(aspect, 2)
rows = db.announces.get_announces(aspect=aspect)
hashes = {r["destination_hash"] for r in rows}
assert "01" * 16 in hashes, "favourited announce was wrongly trimmed"
assert "04" * 16 in hashes, "newest announce should be retained"
assert "02" * 16 not in hashes
assert "03" * 16 not in hashes
finally:
_cleanup(db, path)
def test_trim_preserves_contact_announce_by_identity_hash():
"""Announces tied to a saved contact via identity hash must not be dropped."""
db = path = None
try:
db, path = _new_db()
aspect = "lxmf.delivery"
contact_identity = "b" * 32
_insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z", contact_identity)
_insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
_insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
_insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
db.contacts.add_contact(
name="Friend",
remote_identity_hash=contact_identity,
)
db.announces.trim_announces_for_aspect(aspect, 2)
rows = db.announces.get_announces(aspect=aspect)
hashes = {r["destination_hash"] for r in rows}
assert "01" * 16 in hashes, "contact-linked announce was wrongly trimmed"
assert "04" * 16 in hashes
finally:
_cleanup(db, path)
def test_trim_preserves_contact_announce_by_lxmf_address():
"""Announces matching contacts.lxmf_address must be retained."""
db = path = None
try:
db, path = _new_db()
aspect = "lxmf.delivery"
protected_dest = "01" * 16
_insert(db, protected_dest, aspect, "2000-01-01T00:00:00Z")
_insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
_insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
_insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
db.contacts.add_contact(
name="Friend",
remote_identity_hash="c" * 32,
lxmf_address=protected_dest,
)
db.announces.trim_announces_for_aspect(aspect, 2)
rows = db.announces.get_announces(aspect=aspect)
hashes = {r["destination_hash"] for r in rows}
assert protected_dest in hashes
assert "04" * 16 in hashes
finally:
_cleanup(db, path)