Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Merge the two migration heads left by #1440 and #1444.

Both branches revised `v5o6b7s8d9e0`: #1444 added `w6k7i8n9d0a1` (search_index
uniqueness keyed on the row kind) and #1440 added `w6r7e8a9d0y1` then
`x7d8e9f0a1b2` (project.last_indexed_at, entity.vector_sync_deferred_at). Each
PR was green on its own, and the test suite builds schemas with `create_all` and
stamps them, so nothing ran `upgrade head` against the merged graph. With two
heads, Alembic refuses `upgrade head` and every fresh database fails to
initialize.

A merge revision, rather than re-parenting one branch under the other, is what
keeps databases already stamped at either head upgradeable: from `w6k7i8n9d0a1`
the `w6r7鈥/`x7d8鈥 pair still applies, and from `x7d8e9f0a1b2` the row-kind
index still applies. The schema itself needs no change here.

Revision ID: y8f9a0b1c2d3
Revises: w6k7i8n9d0a1, x7d8e9f0a1b2
Create Date: 2026-09-03 10:45:00.000000

"""

from typing import Sequence, Union


revision: str = "y8f9a0b1c2d3"
down_revision: Union[str, Sequence[str], None] = ("w6k7i8n9d0a1", "x7d8e9f0a1b2")
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Both parents already applied their schema changes; this only joins them."""


def downgrade() -> None:
"""Splitting back into two heads needs no schema change either."""
45 changes: 45 additions & 0 deletions tests/test_migration_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""The migration graph must have exactly one head.

Two PRs that each add a migration off the same parent are each green on their
own and leave main with two heads once both merge. The suite builds schemas with
`create_all` and stamps them, so nothing here ran `upgrade head` against the
merged graph; the first fresh database on main failed to initialize instead
(#1440 + #1444, repaired by the `y8f9a0b1c2d3` merge revision).

This reads the graph the same way `run_migrations` does, so the check fails in
CI on the second PR to merge rather than on the next user's first `bm` command.
"""

from pathlib import Path

from alembic.config import Config
from alembic.script import ScriptDirectory

import basic_memory


def _script_directory() -> ScriptDirectory:
config = Config()
config.set_main_option("script_location", str(Path(basic_memory.__file__).parent / "alembic"))
return ScriptDirectory.from_config(config)


def test_the_migration_graph_has_one_head():
heads = _script_directory().get_heads()

assert len(heads) == 1, (
f"alembic has {len(heads)} heads {sorted(heads)}; `upgrade head` refuses to run "
"until they are joined by a merge revision (down_revision = (a, b))"
)


def test_every_revision_is_reachable_from_the_head():
script = _script_directory()
(head,) = script.get_heads()

reachable = {rev.revision for rev in script.walk_revisions("base", head)}
all_revisions = {rev.revision for rev in script.walk_revisions()}

assert reachable == all_revisions, (
f"revisions not on the path from base to {head}: {sorted(all_revisions - reachable)}"
)
5 changes: 3 additions & 2 deletions tests/test_search_index_row_kind_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ def test_upgrade_widens_the_unique_index_to_include_the_row_kind(monkeypatch) ->
assert len(statements) == 2
# The wide index is created before the narrow one is dropped, so the table is never
# momentarily unconstrained.
assert "CREATE UNIQUE INDEX IF NOT EXISTS uix_search_index_permalink_type_project" in (
statements[0]
assert (
"CREATE UNIQUE INDEX IF NOT EXISTS uix_search_index_permalink_type_project"
in (statements[0])
)
assert "ON search_index (permalink, type, project_id)" in statements[0]
assert "WHERE permalink IS NOT NULL" in statements[0]
Expand Down
Loading