UAT: MigrationRunner.get_pending_migrations() uses linear walk that breaks with multi-head migration graph #5443

Open
opened 2026-04-09 06:51:14 +00:00 by HAL9000 · 1 comment
Owner

Bug Report

Feature Area: Database / Migrations — Migration Versioning
Severity: Backlog — correctness issue with multi-head graphs
Found by: UAT Testing (database-migrations worker)


Summary

MigrationRunner.get_pending_migrations() uses a simple linear walk of script_dir.walk_revisions() to determine pending migrations. This approach is incorrect when the migration graph has multiple heads (merge points), which the codebase already has (5 merge migrations: a7_002, c0_002, d0_002, m6_002, m8_002, 71cd40eb661f).

Evidence

get_pending_migrations() (migration_runner.py:158-188):

def get_pending_migrations(self) -> list[str]:
    current_rev = self.get_current_revision()
    script_dir = ScriptDirectory.from_config(self.alembic_cfg)
    head = script_dir.get_current_head()

    # If current revision matches head, nothing is pending
    if current_rev == head:
        return []
    ...
    # Walk from head backwards, collecting revisions until we hit current
    pending: list[str] = []
    for rev in script_dir.walk_revisions():
        if rev.revision == current_rev:
            break
        pending.append(rev.revision)

Problem 1: script_dir.get_current_head() returns None when there are multiple heads. The migration graph has multiple merge points, so get_current_head() may return None rather than the single head.

Problem 2: The linear walk breaks at current_rev — but in a branched graph, walk_revisions() may not visit current_rev before visiting all pending revisions, causing the loop to never break and returning all revisions as "pending."

Existing merge migrations (confirming multi-head graph):

  • a7_002_merge_heads.py
  • c0_002_merge_skill_registry_head.py
  • d0_002_merge_changeset_and_locks.py
  • m6_002_merge_safety_and_checkpoint.py
  • m8_002_merge_profile_rename_and_corrections.py
  • 71cd40eb661f_merge_resource_links_and_automation_.py

Impact

check_migrations_needed() calls get_pending_migrations(), which is called by init_or_upgrade(). If get_pending_migrations() incorrectly reports pending migrations (or fails to detect them), the application may:

  1. Prompt for migration approval when none are needed
  2. Fail to detect actual pending migrations
  3. Crash with None comparison when get_current_head() returns None

Expected Behavior

Use Alembic's built-in multi-head-aware APIs:

from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory

def get_pending_migrations(self) -> list[str]:
    engine = create_engine(self.database_url)
    with engine.connect() as conn:
        context = MigrationContext.configure(conn)
        current_heads = context.get_current_heads()  # Returns set of current heads
    
    script_dir = ScriptDirectory.from_config(self.alembic_cfg)
    # Use Alembic's revision comparison to find pending revisions
    pending = []
    for rev in script_dir.walk_revisions():
        if rev.revision not in current_heads:
            pending.append(rev.revision)
    return pending

Or more robustly, use alembic.runtime.migration.MigrationContext.get_current_heads() and compare against script_dir.get_heads().

Code Location

  • src/cleveragents/infrastructure/database/migration_runner.py:158-188

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: uat-tester

## Bug Report **Feature Area**: Database / Migrations — Migration Versioning **Severity**: Backlog — correctness issue with multi-head graphs **Found by**: UAT Testing (database-migrations worker) --- ## Summary `MigrationRunner.get_pending_migrations()` uses a simple linear walk of `script_dir.walk_revisions()` to determine pending migrations. This approach is incorrect when the migration graph has multiple heads (merge points), which the codebase already has (5 merge migrations: `a7_002`, `c0_002`, `d0_002`, `m6_002`, `m8_002`, `71cd40eb661f`). ## Evidence **`get_pending_migrations()`** (migration_runner.py:158-188): ```python def get_pending_migrations(self) -> list[str]: current_rev = self.get_current_revision() script_dir = ScriptDirectory.from_config(self.alembic_cfg) head = script_dir.get_current_head() # If current revision matches head, nothing is pending if current_rev == head: return [] ... # Walk from head backwards, collecting revisions until we hit current pending: list[str] = [] for rev in script_dir.walk_revisions(): if rev.revision == current_rev: break pending.append(rev.revision) ``` **Problem 1**: `script_dir.get_current_head()` returns `None` when there are multiple heads. The migration graph has multiple merge points, so `get_current_head()` may return `None` rather than the single head. **Problem 2**: The linear walk breaks at `current_rev` — but in a branched graph, `walk_revisions()` may not visit `current_rev` before visiting all pending revisions, causing the loop to never break and returning all revisions as "pending." **Existing merge migrations** (confirming multi-head graph): - `a7_002_merge_heads.py` - `c0_002_merge_skill_registry_head.py` - `d0_002_merge_changeset_and_locks.py` - `m6_002_merge_safety_and_checkpoint.py` - `m8_002_merge_profile_rename_and_corrections.py` - `71cd40eb661f_merge_resource_links_and_automation_.py` ## Impact `check_migrations_needed()` calls `get_pending_migrations()`, which is called by `init_or_upgrade()`. If `get_pending_migrations()` incorrectly reports pending migrations (or fails to detect them), the application may: 1. Prompt for migration approval when none are needed 2. Fail to detect actual pending migrations 3. Crash with `None` comparison when `get_current_head()` returns `None` ## Expected Behavior Use Alembic's built-in multi-head-aware APIs: ```python from alembic.runtime.migration import MigrationContext from alembic.script import ScriptDirectory def get_pending_migrations(self) -> list[str]: engine = create_engine(self.database_url) with engine.connect() as conn: context = MigrationContext.configure(conn) current_heads = context.get_current_heads() # Returns set of current heads script_dir = ScriptDirectory.from_config(self.alembic_cfg) # Use Alembic's revision comparison to find pending revisions pending = [] for rev in script_dir.walk_revisions(): if rev.revision not in current_heads: pending.append(rev.revision) return pending ``` Or more robustly, use `alembic.runtime.migration.MigrationContext.get_current_heads()` and compare against `script_dir.get_heads()`. ## Code Location - `src/cleveragents/infrastructure/database/migration_runner.py:158-188` --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: uat-tester
HAL9000 added this to the v3.5.0 milestone 2026-04-09 06:59:58 +00:00
Author
Owner

Label compliance fix applied:

  • Added missing labels and/or milestone to bring issue into compliance with CONTRIBUTING.md

Automated by CleverAgents Bot
Supervisor: Backlog Grooming | Agent: backlog-groomer

Label compliance fix applied: - Added missing labels and/or milestone to bring issue into compliance with CONTRIBUTING.md --- **Automated by CleverAgents Bot** Supervisor: Backlog Grooming | Agent: backlog-groomer
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#5443
No description provided.