refactor(auth): remove SQL orphan migration (unused in supported scenarios)

The _migrate_orphan_sql_tables helper existed to bind NULL owner_id
rows in threads_meta, runs, run_events, and feedback to the admin on
first boot. But in every supported upgrade path, it's a no-op:

  1. Fresh install: create_all builds fresh tables, no legacy rows
  2. No-auth → with-auth (no existing persistence DB): persistence
     tables are created fresh by create_all, no legacy rows
  3. No-auth → with-auth (has existing persistence DB from #1930):
     NOT a supported upgrade path — "有 DB 到有 DB" schema evolution
     is out of scope; users wipe DB or run manual ALTER

So the SQL orphan migration never has anything to do in the
supported matrix. Delete the function, simplify _ensure_admin_user
from a 3-step pipeline to a 2-step one (admin creation + LangGraph
store orphan migration only).

LangGraph store orphan migration stays: it serves the real
"no-auth → with-auth" upgrade path where a user's existing LangGraph
thread metadata has no owner_id field and needs to be stamped with
the newly-created admin's id.

Tests: 284 passed (auth + persistence + isolation)
Lint: clean
This commit is contained in:
greatmengqi 2026-04-08 12:16:49 +08:00
parent ceeccabc98
commit ea73db6fc1

View File

@ -43,19 +43,21 @@ logger = logging.getLogger(__name__)
async def _ensure_admin_user(app: FastAPI) -> None:
"""Auto-create the admin user on first boot if no users exist.
After admin creation (or on every boot), run a three-step orphan
migration pipeline:
After admin creation, migrate orphan threads from the LangGraph
store (metadata.owner_id unset) to the admin account. This is the
"no-auth → with-auth" upgrade path: users who ran DeerFlow without
authentication have existing LangGraph thread data that needs an
owner assigned.
1. Fatal: admin creation (can't proceed without an admin user)
2. Non-fatal: LangGraph store orphan threads (cursor-paginated)
3. Non-fatal: SQL persistence tables (threads_meta, runs, run_events,
feedback) every row with owner_id IS NULL gets bound to admin
No SQL persistence migration is needed: the four owner_id columns
(threads_meta, runs, run_events, feedback) only come into existence
alongside the auth module via create_all, so freshly created tables
never contain NULL-owner rows. "Existing persistence DB + new auth"
is not a supported upgrade path fresh install or wipe-and-retry.
Multi-worker safe: relies on SQLite UNIQUE constraint to resolve
races during admin creation. Only the worker that successfully
creates/updates the admin prints the password; losers silently skip.
The orphan migration steps are idempotent a second call finds
nothing to migrate and returns 0.
"""
import secrets
@ -104,7 +106,9 @@ async def _ensure_admin_user(app: FastAPI) -> None:
admin_id = str(admin.id)
# Step 2: LangGraph store orphan migration — non-fatal
# LangGraph store orphan migration — non-fatal.
# This covers the "no-auth → with-auth" upgrade path for users
# whose existing LangGraph thread metadata has no owner_id set.
store = getattr(app.state, "store", None)
if store is not None:
try:
@ -114,12 +118,6 @@ async def _ensure_admin_user(app: FastAPI) -> None:
except Exception:
logger.exception("LangGraph thread migration failed (non-fatal)")
# Step 3: SQL persistence tables — non-fatal
try:
await _migrate_orphan_sql_tables(admin_id)
except Exception:
logger.exception("SQL persistence migration failed (non-fatal)")
if fresh_admin_created:
logger.info("=" * 60)
logger.info(" Admin account created on first boot")
@ -166,45 +164,6 @@ async def _migrate_orphaned_threads(store, admin_user_id: str) -> int:
return migrated
async def _migrate_orphan_sql_tables(admin_user_id: str) -> None:
"""Bind NULL owner_id rows in the 4 SQL persistence tables to admin.
Runs in a single transaction per table via the shared async session
factory. Each UPDATE is idempotent a second call finds nothing to
migrate (rowcount=0).
"""
from sqlalchemy import update
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.feedback.model import FeedbackRow
from deerflow.persistence.models.run_event import RunEventRow
from deerflow.persistence.run.model import RunRow
from deerflow.persistence.thread_meta.model import ThreadMetaRow
sf = get_session_factory()
if sf is None:
# In-memory / no persistence backend — nothing to migrate.
return
tables = [
(ThreadMetaRow, "threads_meta"),
(RunRow, "runs"),
(RunEventRow, "run_events"),
(FeedbackRow, "feedback"),
]
async with sf() as session:
for model, label in tables:
stmt = update(model).where(model.owner_id.is_(None)).values(owner_id=admin_user_id)
result = await session.execute(stmt)
count = result.rowcount or 0
if count > 0:
logger.info("Migrated %d orphan %s row(s) to admin", count, label)
else:
logger.debug("No orphan %s rows to migrate", label)
await session.commit()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan handler."""