fix(migrations): add schema parity removal and merge migrations

Added two new migrations to resolve database schema issues:
- m8_003_remove_schema_parity_changes: Removes schema parity changes (link_type column, partial index, foreign keys) that were deleted from the models but not from the database.
- m9_003_merge_schema_and_action_heads: Merges the two migration heads (m8_003 and a5_006) into a single head to resolve Alembic branching issue.

This fixes the database creation timeout issue that was preventing the test suite from running.
This commit is contained in:
2026-04-22 07:19:01 +00:00
committed by drew
parent ba99cd8274
commit a70a520026
2 changed files with 193 additions and 0 deletions
@@ -0,0 +1,160 @@
"""Remove schema parity changes from resource_links and checkpoint_metadata.
Reverts the schema changes that were intended to be added by
m4_004_schema_parity_resource_decision_checkpoint:
1. Remove ``link_type`` column from ``resource_links`` table.
2. Remove CHECK constraint on ``link_type`` from ``resource_links``.
3. Remove partial index ``idx_decisions_superseded`` from ``decisions`` table.
4. Remove foreign key constraints from ``checkpoint_metadata`` table.
Revision ID: m8_003_remove_schema_parity_changes
Revises: m8_002_merge_profile_rename_and_corrections
Create Date: 2026-04-22 00:00:00
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "m8_003_remove_schema_parity_changes"
down_revision: str | Sequence[str] | None = (
"m8_002_merge_profile_rename_and_corrections"
)
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _inspector() -> sa.Inspector:
return sa.inspect(op.get_bind())
def upgrade() -> None:
"""Remove schema parity changes."""
inspector = _inspector()
# Remove link_type column from resource_links if it exists
resource_link_columns = {
column["name"] for column in inspector.get_columns("resource_links")
}
if "link_type" in resource_link_columns:
check_constraints = inspector.get_check_constraints("resource_links")
has_link_type_ck = any(
ck.get("name") == "ck_resource_links_link_type"
for ck in check_constraints
)
with op.batch_alter_table("resource_links") as batch_op:
if has_link_type_ck:
batch_op.drop_constraint(
"ck_resource_links_link_type", type_="check"
)
batch_op.drop_column("link_type")
# Remove partial index from decisions if it exists
decision_indexes = {
index["name"] for index in inspector.get_indexes("decisions")
}
if "idx_decisions_superseded" in decision_indexes:
op.drop_index("idx_decisions_superseded", table_name="decisions")
# Remove foreign key constraints from checkpoint_metadata if they exist
checkpoint_fks = inspector.get_foreign_keys("checkpoint_metadata")
fk_names = {fk.get("name") for fk in checkpoint_fks}
if (
"fk_checkpoint_metadata_decision" in fk_names
or "fk_checkpoint_metadata_resource" in fk_names
):
with op.batch_alter_table("checkpoint_metadata") as batch_op:
if "fk_checkpoint_metadata_decision" in fk_names:
batch_op.drop_constraint(
"fk_checkpoint_metadata_decision",
type_="foreignkey",
)
if "fk_checkpoint_metadata_resource" in fk_names:
batch_op.drop_constraint(
"fk_checkpoint_metadata_resource",
type_="foreignkey",
)
def downgrade() -> None:
"""Restore schema parity changes."""
inspector = _inspector()
# Restore link_type column to resource_links
resource_link_columns = {
column["name"] for column in inspector.get_columns("resource_links")
}
if "link_type" not in resource_link_columns:
with op.batch_alter_table("resource_links") as batch_op:
batch_op.add_column(
sa.Column(
"link_type",
sa.Text(),
nullable=False,
server_default=sa.text("'contains'"),
)
)
batch_op.create_check_constraint(
"ck_resource_links_link_type",
"link_type IN ('contains', 'references', 'derived_from')",
)
# Restore partial index on decisions
decision_indexes = {
index["name"] for index in inspector.get_indexes("decisions")
}
if "idx_decisions_superseded" not in decision_indexes:
op.create_index(
"idx_decisions_superseded",
"decisions",
["superseded_by"],
unique=False,
postgresql_where=sa.text("superseded_by IS NOT NULL"),
sqlite_where=sa.text("superseded_by IS NOT NULL"),
)
# Restore foreign key constraints on checkpoint_metadata
checkpoint_fks = inspector.get_foreign_keys("checkpoint_metadata")
fk_signatures = {
(
tuple(fk.get("constrained_columns") or []),
fk.get("referred_table"),
tuple(fk.get("referred_columns") or []),
)
for fk in checkpoint_fks
}
needs_decision_fk = (
("decision_id",),
"decisions",
("decision_id",),
) not in fk_signatures
needs_resource_fk = (
("resource_id",),
"resources",
("resource_id",),
) not in fk_signatures
if needs_decision_fk or needs_resource_fk:
with op.batch_alter_table("checkpoint_metadata") as batch_op:
if needs_decision_fk:
batch_op.create_foreign_key(
"fk_checkpoint_metadata_decision",
"decisions",
["decision_id"],
["decision_id"],
ondelete="SET NULL",
)
if needs_resource_fk:
batch_op.create_foreign_key(
"fk_checkpoint_metadata_resource",
"resources",
["resource_id"],
["resource_id"],
ondelete="SET NULL",
)
@@ -0,0 +1,33 @@
"""Merge schema parity removal and action invariants heads.
m8_003_remove_schema_parity_changes and a5_006_action_invariants_unique_constraint
branched from different parents, creating two Alembic heads.
This no-op merge migration resolves them into a single head.
Revision ID: m9_003_merge_schema_and_action_heads
Revises: m8_003_remove_schema_parity_changes, a5_006_action_invariants_unique_constraint
Create Date: 2026-04-22 00:00:00
"""
from __future__ import annotations
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "m9_003_merge_schema_and_action_heads"
down_revision: str | Sequence[str] | None = (
"m8_003_remove_schema_parity_changes",
"a5_006_action_invariants_unique_constraint",
)
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass