Skip to content

bug: BackendOrchestrator uses DatabaseManager "conn" attribute which doesn't exist, silently breaking schema v3.4.5 migration on every boot #47

@barrygfox

Description

@barrygfox

Summary

BackendOrchestrator accesses self._db.conn in two methods, but DatabaseManager has no .conn attribute — it uses per-call connections. So:

  1. The v3.4.5 schema migration never runs → the access_count_30d column is never added to atomic_facts resulting in silently disabling 30-day recency scoring (F-14).
  2. The _update_status() method then silently fails on every call and the backend_status table is never updated.

Both failures are swallowed by except Exception: pass / logger.warning(...) blocks, so the daemon starts successfully but operates in a degraded state with no visible indication beyond a single non-fatal warning in the log.

Reproduction

# Minimal repro — no daemon needed
import sqlite3
from superlocalmemory.storage.database import DatabaseManager
from superlocalmemory.core.backend_orchestrator import BackendOrchestrator

db = DatabaseManager(":memory:")
# DatabaseManager has no .conn attribute:
assert not hasattr(db, "conn")  # True — AttributeError follows if accessed

# In production the orchestrator catches and logs this on every boot:
# WARNING - Schema v3.4.5 apply failed (non-fatal): 'DatabaseManager' object
#           has no attribute 'conn'

Observed in daemon log on every startup since at least 3.4.5:

Schema v3.4.5 apply failed (non-fatal): 'DatabaseManager' object has no attribute 'conn'

Proposed root cause

DatabaseManager is documented as using per-call connections ("Per-call connections: no shared state between processes") and intentionally exposes no persistent connection. The private _connect() method opens a fresh connection, returns it, and callers are expected to close it.

backend_orchestrator.py:359–371_apply_schema_v345():

if not schema_version_applied(self._db.conn):   # AttributeError here
    result = apply_migration(self._db.conn)      # and here

backend_orchestrator.py:341–353_update_status():

self._db.conn.execute("INSERT OR REPLACE INTO backend_status ...")  # AttributeError here
self._db.conn.commit()                                               # and here

Why tests don't catch it

tests/core/test_orchestrator.py uses a MockDB that provides a .conn attribute:

class MockDB:
    def __init__(self):
        self.conn = sqlite3.connect(":memory:")  # masks the production bug

The tests pass because MockDB.conn exists whereasDatabaseManager.conn does not.

Potential fix

_apply_schema_v345() — open and close a dedicated connection:

def _apply_schema_v345(self) -> None:
    try:
        from superlocalmemory.storage.schema_v345 import (
            apply_migration, schema_version_applied,
        )
        conn = self._db._connect()
        try:
            if not schema_version_applied(conn):
                result = apply_migration(conn)
                conn.commit()
                if result.get("errors"):
                    logger.warning("Schema v3.4.5 had errors: %s", result["errors"])
        finally:
            conn.close()
    except ImportError:
        logger.debug("schema_v345 not found — skipping")
    except Exception as exc:
        logger.warning("Schema v3.4.5 apply failed (non-fatal): %s", exc)

_update_status() — use the public execute() method (handles connection lifecycle internally):

def _update_status(self, name: str, status: str,
                    count: int = 0, error: str = "") -> None:
    self._backend_cache[name] = status
    try:
        self._db.execute(
            "INSERT OR REPLACE INTO backend_status "
            "(backend_name, status, record_count, error_message, last_sync_at) "
            "VALUES (?, ?, ?, ?, datetime('now'))",
            (name, status, count, error),
        )
    except Exception:
        pass

Suggested test additions

def test_apply_schema_v345_with_real_database_manager(tmp_path):
    """Schema migration must succeed when using a real DatabaseManager."""
    from superlocalmemory.storage.database import DatabaseManager
    import sqlite3

    db_path = tmp_path / "test.db"
    db = DatabaseManager(db_path)
    # Ensure backend_status table exists (normally created by engine init)
    conn = db._connect()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS backend_status (
            backend_name TEXT PRIMARY KEY,
            status TEXT DEFAULT 'not_initialized',
            record_count INTEGER DEFAULT 0,
            last_sync_at TEXT,
            error_message TEXT DEFAULT ''
        )
    """)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS atomic_facts (
            fact_id TEXT PRIMARY KEY,
            profile_id TEXT,
            content TEXT,
            created_at TEXT
        )
    """)
    conn.commit()
    conn.close()

    orch = BackendOrchestrator(db=db, config=MockConfig())
    # Must not raise and must not log a warning — the migration should apply cleanly
    orch._apply_schema_v345()

    # Verify the column was actually added
    check_conn = db._connect()
    row = check_conn.execute(
        "SELECT 1 FROM pragma_table_info('atomic_facts') WHERE name='access_count_30d'"
    ).fetchone()
    check_conn.close()
    assert row is not None, "access_count_30d column was not created"


def test_update_status_with_real_database_manager(tmp_path):
    """_update_status must persist to DB when using a real DatabaseManager."""
    from superlocalmemory.storage.database import DatabaseManager

    db_path = tmp_path / "test.db"
    db = DatabaseManager(db_path)
    conn = db._connect()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS backend_status (
            backend_name TEXT PRIMARY KEY,
            status TEXT DEFAULT 'not_initialized',
            record_count INTEGER DEFAULT 0,
            last_sync_at TEXT,
            error_message TEXT DEFAULT ''
        )
    """)
    conn.commit()
    conn.close()

    orch = BackendOrchestrator(db=db, config=MockConfig())
    orch._update_status("cozo", "active", count=42)

    rows = db.execute("SELECT status, record_count FROM backend_status WHERE backend_name = 'cozo'")
    assert len(rows) == 1
    assert dict(rows[0])["status"] == "active"
    assert dict(rows[0])["record_count"] == 42

Note: Additional affected call site

tier_manager.py line 447 has the same pattern (db.conn passed to bulk_update_tiers_from_sqlite). Not included in this issue, but maybe worth fixing in the same pass.

Environment

  • SLM version: 3.6.13 (also present in current main)
  • Python: 3.14
  • Platform: macOS Darwin 25.5.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions