diff --git a/src/basic_memory/alembic/versions/y8f9a0b1c2d3_merge_row_kind_and_deferral_heads.py b/src/basic_memory/alembic/versions/y8f9a0b1c2d3_merge_row_kind_and_deferral_heads.py new file mode 100644 index 000000000..4e1368558 --- /dev/null +++ b/src/basic_memory/alembic/versions/y8f9a0b1c2d3_merge_row_kind_and_deferral_heads.py @@ -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.""" diff --git a/tests/test_migration_graph.py b/tests/test_migration_graph.py new file mode 100644 index 000000000..cc629a728 --- /dev/null +++ b/tests/test_migration_graph.py @@ -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)}" + ) diff --git a/tests/test_search_index_row_kind_migration.py b/tests/test_search_index_row_kind_migration.py index 8a79b4fde..633fd366e 100644 --- a/tests/test_search_index_row_kind_migration.py +++ b/tests/test_search_index_row_kind_migration.py @@ -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]