"""add repair parts tracking Revision ID: 0014_repair_parts_tracking Revises: 0013_control_board_replacement Create Date: 2026-08-02 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = "0014_repair_parts_tracking" down_revision: Union[str, None] = "0013_control_board_replacement" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None COMPONENT_TYPES = ("control_board", "hashboard", "power_supply", "fan") CONSUMABLE_UNITS = ("each", "gram", "millilitre", "metre", "centimetre") REPAIR_REASONS = ( "HARDWARE_FAILURE", "LOW_HASHRATE", "CONTROL_BOARD", "HASHBOARD", "PSU", "FAN", "OVERHEATING", "NETWORKING", "PREVENTIVE_MAINTENANCE", "INSPECTION", "OTHER", ) REPLACEMENT_REASONS = ( "failed", "upgrade", "diagnostic_swap", "preventive_maintenance", "temporary_donor", "unknown", ) DISPOSITIONS = ( "repair_inventory", "sent_for_repair", "scrapped", "retained_for_diagnosis", "returned_to_vendor", "unknown", ) def _quoted(values: Sequence[str]) -> str: return ", ".join(f"'{value}'" for value in values) def _create_event_constraints(include_parts: bool) -> None: event_types = [ "REPAIR_STARTED", "INITIAL_NOTE", "REPAIR_COMPLETED", "CONTROL_BOARD_REPLACED", ] if include_parts: event_types.extend(("REPAIR_PART_RECORDED", "REPAIR_PART_VOIDED")) op.create_check_constraint( "ck_repair_events_type", "repair_events", f"event_type IN ({_quoted(event_types)})", ) parts_clause = "" if include_parts: parts_clause = ( ") OR (event_type = 'REPAIR_PART_RECORDED' " "AND payload ?& ARRAY['repair_part_id', 'entry_kind', 'notes'] " "AND payload ->> 'repair_part_id' ~ " "'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-" "[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' " "AND (jsonb_typeof(payload -> 'notes') = 'null' OR (" "jsonb_typeof(payload -> 'notes') = 'string' " "AND trim(payload ->> 'notes') <> '' " "AND char_length(payload ->> 'notes') <= 4000)) " "AND ((payload ->> 'entry_kind' = 'consumable' " "AND payload ?& ARRAY['description', 'quantity', 'unit'] " "AND NOT payload ? 'component_id' " "AND jsonb_typeof(payload -> 'description') = 'string' " "AND trim(payload ->> 'description') <> '' " "AND char_length(payload ->> 'description') <= 255 " "AND jsonb_typeof(payload -> 'quantity') = 'string' " "AND payload ->> 'quantity' ~ '^[0-9]{1,9}(\\.[0-9]{1,3})?$' " "AND (payload ->> 'quantity')::numeric > 0 " f"AND payload ->> 'unit' IN ({_quoted(CONSUMABLE_UNITS)}) " "AND (payload ->> 'unit' <> 'each' OR " "(payload ->> 'quantity')::numeric = " "trunc((payload ->> 'quantity')::numeric))) OR (" "payload ->> 'entry_kind' = 'tracked_component' " "AND payload ?& ARRAY['component_id', 'component_type', " "'component_asset_id', 'component_manufacturer', " "'component_model', 'component_manufacturer_serial'] " "AND NOT payload ? 'description' " "AND NOT payload ? 'quantity' " "AND NOT payload ? 'unit' " "AND payload ->> 'component_id' ~ " "'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-" "[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' " f"AND payload ->> 'component_type' IN ({_quoted(COMPONENT_TYPES)}) " "AND jsonb_typeof(payload -> 'component_asset_id') IN ('string', 'null') " "AND jsonb_typeof(payload -> 'component_manufacturer') IN ('string', 'null') " "AND jsonb_typeof(payload -> 'component_model') IN ('string', 'null') " "AND jsonb_typeof(payload -> 'component_manufacturer_serial') " "IN ('string', 'null'))))" " OR (event_type = 'REPAIR_PART_VOIDED' " "AND payload ?& ARRAY['repair_part_id', 'reason'] " "AND payload ->> 'repair_part_id' ~ " "'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-" "[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' " "AND jsonb_typeof(payload -> 'reason') = 'string' " "AND trim(payload ->> 'reason') <> '' " "AND char_length(payload ->> 'reason') <= 1000" ) op.create_check_constraint( "ck_repair_events_payload_shape", "repair_events", "(event_type = 'REPAIR_STARTED' " "AND payload ?& ARRAY['reason', 'from_lifecycle', 'to_lifecycle'] " f"AND payload ->> 'reason' IN ({_quoted(REPAIR_REASONS)}) " "AND payload ->> 'from_lifecycle' IN ('active', 'spare') " "AND payload ->> 'to_lifecycle' = 'in_repair') OR (" "event_type = 'INITIAL_NOTE' " "AND jsonb_typeof(payload -> 'note') = 'string' " "AND trim(payload ->> 'note') <> '') OR (" "event_type = 'REPAIR_COMPLETED' " "AND payload ?& ARRAY['previous_miner_lifecycle', 'resulting_miner_lifecycle'] " "AND payload ->> 'previous_miner_lifecycle' = 'in_repair' " "AND payload ->> 'resulting_miner_lifecycle' IN ('active', 'spare') " "AND (NOT payload ? 'pre_repair_lifecycle' " "OR payload ->> 'pre_repair_lifecycle' IN ('active', 'spare')) " "AND (NOT payload ? 'completion_summary' OR (" "jsonb_typeof(payload -> 'completion_summary') = 'string' " "AND trim(payload ->> 'completion_summary') <> '' " "AND char_length(payload ->> 'completion_summary') <= 4000))) OR (" "event_type = 'CONTROL_BOARD_REPLACED' " "AND payload ?& ARRAY['old_mac_address', 'new_mac_address', " "'old_control_board_serial', 'new_control_board_serial', " "'replacement_reason', 'removed_board_disposition'] " "AND jsonb_typeof(payload -> 'old_mac_address') IN ('string', 'null') " "AND jsonb_typeof(payload -> 'new_mac_address') = 'string' " "AND payload ->> 'new_mac_address' ~ '^([0-9A-F]{2}:){5}[0-9A-F]{2}$' " "AND regexp_replace(lower(payload ->> 'new_mac_address'), '[^0-9a-f]', '', 'g') " "NOT IN ('000000000000', 'ffffffffffff') " "AND get_byte(decode(regexp_replace(lower(payload ->> 'new_mac_address'), " "'[^0-9a-f]', '', 'g'), 'hex'), 0) % 2 = 0 " "AND (jsonb_typeof(payload -> 'old_control_board_serial') = 'null' OR (" "jsonb_typeof(payload -> 'old_control_board_serial') = 'string' " "AND trim(payload ->> 'old_control_board_serial') <> '' " "AND char_length(payload ->> 'old_control_board_serial') <= 255)) " "AND (jsonb_typeof(payload -> 'new_control_board_serial') = 'null' OR (" "jsonb_typeof(payload -> 'new_control_board_serial') = 'string' " "AND trim(payload ->> 'new_control_board_serial') <> '' " "AND char_length(payload ->> 'new_control_board_serial') <= 255)) " f"AND payload ->> 'replacement_reason' IN ({_quoted(REPLACEMENT_REASONS)}) " f"AND payload ->> 'removed_board_disposition' IN ({_quoted(DISPOSITIONS)}) " "AND (NOT payload ? 'notes' OR jsonb_typeof(payload -> 'notes') = 'null' OR (" "jsonb_typeof(payload -> 'notes') = 'string' " "AND trim(payload ->> 'notes') <> '' " "AND char_length(payload ->> 'notes') <= 4000))" f"{parts_clause})", ) def upgrade() -> None: connection = op.get_bind() connection.exec_driver_sql( """ INSERT INTO permissions (key, description) VALUES ('components.read', 'View tracked component identity'), ('components.create', 'Register tracked component identity'), ('repairs.parts.manage', 'Record and void repair-part usage') ON CONFLICT (key) DO UPDATE SET description = EXCLUDED.description; INSERT INTO role_permissions (role_id, permission_id) SELECT roles.id, permissions.id FROM roles JOIN permissions ON permissions.key IN ( 'components.read', 'components.create', 'repairs.parts.manage' ) WHERE roles.name IN ('Admin', 'Manager', 'Technician') ON CONFLICT DO NOTHING; INSERT INTO role_permissions (role_id, permission_id) SELECT roles.id, permissions.id FROM roles JOIN permissions ON permissions.key = 'components.read' WHERE roles.name = 'Read Only' ON CONFLICT DO NOTHING; """ ) op.create_table( "components", sa.Column( "id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()"), ), sa.Column("component_type", sa.Text(), nullable=False), sa.Column("asset_id", sa.Text(), nullable=True), sa.Column("manufacturer", sa.Text(), nullable=True), sa.Column("model", sa.Text(), nullable=True), sa.Column("manufacturer_serial", sa.Text(), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="RESTRICT"), nullable=False, ), sa.CheckConstraint( f"component_type IN ({_quoted(COMPONENT_TYPES)})", name="ck_components_type", ), sa.CheckConstraint( "asset_id IS NULL OR (trim(asset_id) <> '' " "AND asset_id = trim(asset_id) AND char_length(asset_id) <= 255)", name="ck_components_asset_id", ), sa.CheckConstraint( "manufacturer IS NULL OR (trim(manufacturer) <> '' " "AND manufacturer = trim(manufacturer) " "AND char_length(manufacturer) <= 255)", name="ck_components_manufacturer", ), sa.CheckConstraint( "model IS NULL OR (trim(model) <> '' AND model = trim(model) " "AND char_length(model) <= 255)", name="ck_components_model", ), sa.CheckConstraint( "manufacturer_serial IS NULL OR (trim(manufacturer_serial) <> '' " "AND manufacturer_serial = trim(manufacturer_serial) " "AND char_length(manufacturer_serial) <= 255)", name="ck_components_manufacturer_serial", ), ) op.create_index("idx_components_created_by", "components", ["created_by"]) op.create_index("idx_components_type", "components", ["component_type"]) op.create_index( "uq_components_normalized_asset_id", "components", [sa.text("lower(trim(asset_id))")], unique=True, postgresql_where=sa.text("asset_id IS NOT NULL"), ) op.create_table( "repair_parts", sa.Column( "id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()"), ), sa.Column( "repair_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("repairs.id", ondelete="RESTRICT"), nullable=False, ), sa.Column("consumable_description", sa.Text(), nullable=True), sa.Column("quantity", sa.Numeric(precision=12, scale=3), nullable=True), sa.Column("unit", sa.Text(), nullable=True), sa.Column( "component_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("components.id", ondelete="RESTRICT"), nullable=True, ), sa.Column("component_type_snapshot", sa.Text(), nullable=True), sa.Column("component_asset_id_snapshot", sa.Text(), nullable=True), sa.Column("component_manufacturer_snapshot", sa.Text(), nullable=True), sa.Column("component_model_snapshot", sa.Text(), nullable=True), sa.Column("component_manufacturer_serial_snapshot", sa.Text(), nullable=True), sa.Column("notes", sa.Text(), nullable=True), sa.Column( "recorded_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "recorded_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "recorded_event_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("repair_events.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "voided_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="RESTRICT"), nullable=True, ), sa.Column("voided_at", sa.DateTime(timezone=True), nullable=True), sa.Column("void_reason", sa.Text(), nullable=True), sa.Column( "void_event_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("repair_events.id", ondelete="RESTRICT"), nullable=True, ), sa.CheckConstraint( "((consumable_description IS NOT NULL AND quantity IS NOT NULL " "AND unit IS NOT NULL AND component_id IS NULL " "AND component_type_snapshot IS NULL " "AND component_asset_id_snapshot IS NULL " "AND component_manufacturer_snapshot IS NULL " "AND component_model_snapshot IS NULL " "AND component_manufacturer_serial_snapshot IS NULL) OR " "(consumable_description IS NULL AND quantity IS NULL " "AND unit IS NULL AND component_id IS NOT NULL " "AND component_type_snapshot IS NOT NULL))", name="ck_repair_parts_entry_xor", ), sa.CheckConstraint( "quantity IS NULL OR quantity > 0", name="ck_repair_parts_quantity_positive", ), sa.CheckConstraint( "unit IS NULL OR unit <> 'each' OR quantity = trunc(quantity)", name="ck_repair_parts_each_whole", ), sa.CheckConstraint( f"unit IS NULL OR unit IN ({_quoted(CONSUMABLE_UNITS)})", name="ck_repair_parts_unit", ), sa.CheckConstraint( f"component_type_snapshot IS NULL OR " f"component_type_snapshot IN ({_quoted(COMPONENT_TYPES)})", name="ck_repair_parts_component_type", ), sa.CheckConstraint( "consumable_description IS NULL OR (" "consumable_description = trim(consumable_description) " "AND trim(consumable_description) <> '' " "AND char_length(consumable_description) <= 255)", name="ck_repair_parts_description", ), sa.CheckConstraint( "component_asset_id_snapshot IS NULL OR (" "component_asset_id_snapshot = trim(component_asset_id_snapshot) " "AND trim(component_asset_id_snapshot) <> '' " "AND char_length(component_asset_id_snapshot) <= 255)", name="ck_repair_parts_asset_snapshot", ), sa.CheckConstraint( "component_manufacturer_snapshot IS NULL OR (" "component_manufacturer_snapshot = trim(component_manufacturer_snapshot) " "AND trim(component_manufacturer_snapshot) <> '' " "AND char_length(component_manufacturer_snapshot) <= 255)", name="ck_repair_parts_manufacturer_snapshot", ), sa.CheckConstraint( "component_model_snapshot IS NULL OR (" "component_model_snapshot = trim(component_model_snapshot) " "AND trim(component_model_snapshot) <> '' " "AND char_length(component_model_snapshot) <= 255)", name="ck_repair_parts_model_snapshot", ), sa.CheckConstraint( "component_manufacturer_serial_snapshot IS NULL OR (" "component_manufacturer_serial_snapshot = " "trim(component_manufacturer_serial_snapshot) " "AND trim(component_manufacturer_serial_snapshot) <> '' " "AND char_length(component_manufacturer_serial_snapshot) <= 255)", name="ck_repair_parts_serial_snapshot", ), sa.CheckConstraint( "notes IS NULL OR (notes = trim(notes) AND trim(notes) <> '' " "AND char_length(notes) <= 4000)", name="ck_repair_parts_notes", ), sa.CheckConstraint( "(voided_by IS NULL AND voided_at IS NULL AND void_reason IS NULL " "AND void_event_id IS NULL) OR " "(voided_by IS NOT NULL AND voided_at IS NOT NULL " "AND void_reason IS NOT NULL AND void_event_id IS NOT NULL)", name="ck_repair_parts_void_state", ), sa.CheckConstraint( "void_reason IS NULL OR (void_reason = trim(void_reason) " "AND trim(void_reason) <> '' AND char_length(void_reason) <= 1000)", name="ck_repair_parts_void_reason", ), sa.CheckConstraint( "voided_at IS NULL OR voided_at >= recorded_at", name="ck_repair_parts_void_chronology", ), ) op.create_index( "idx_repair_parts_repair", "repair_parts", ["repair_id", "recorded_event_id", "id"], ) op.create_index("idx_repair_parts_component", "repair_parts", ["component_id"]) op.create_index("idx_repair_parts_recorded_by", "repair_parts", ["recorded_by"]) op.create_index("idx_repair_parts_voided_by", "repair_parts", ["voided_by"]) op.create_index( "uq_repair_parts_recorded_event", "repair_parts", ["recorded_event_id"], unique=True, ) op.create_index( "uq_repair_parts_void_event", "repair_parts", ["void_event_id"], unique=True, postgresql_where=sa.text("void_event_id IS NOT NULL"), ) op.drop_constraint("ck_repair_events_payload_shape", "repair_events", type_="check") op.drop_constraint("ck_repair_events_type", "repair_events", type_="check") _create_event_constraints(True) connection.exec_driver_sql( """ CREATE FUNCTION minerops_protect_component_identity() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'Durable component identity cannot be deleted'; END IF; IF NEW.id <> OLD.id OR NEW.component_type <> OLD.component_type OR NEW.created_at <> OLD.created_at OR NEW.created_by <> OLD.created_by THEN RAISE EXCEPTION 'Component identity, type, and creation provenance are immutable'; END IF; RETURN NEW; END $$; CREATE TRIGGER trg_protect_component_identity BEFORE UPDATE OR DELETE ON components FOR EACH ROW EXECUTE FUNCTION minerops_protect_component_identity(); CREATE FUNCTION minerops_protect_repair_event_history() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Repair event history is immutable'; END $$; CREATE TRIGGER trg_protect_repair_event_history BEFORE UPDATE OR DELETE ON repair_events FOR EACH ROW EXECUTE FUNCTION minerops_protect_repair_event_history(); CREATE FUNCTION minerops_protect_repair_part_history() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'Repair-part history is immutable'; END IF; IF NEW.id <> OLD.id OR NEW.repair_id <> OLD.repair_id OR NEW.consumable_description IS DISTINCT FROM OLD.consumable_description OR NEW.quantity IS DISTINCT FROM OLD.quantity OR NEW.unit IS DISTINCT FROM OLD.unit OR NEW.component_id IS DISTINCT FROM OLD.component_id OR NEW.component_type_snapshot IS DISTINCT FROM OLD.component_type_snapshot OR NEW.component_asset_id_snapshot IS DISTINCT FROM OLD.component_asset_id_snapshot OR NEW.component_manufacturer_snapshot IS DISTINCT FROM OLD.component_manufacturer_snapshot OR NEW.component_model_snapshot IS DISTINCT FROM OLD.component_model_snapshot OR NEW.component_manufacturer_serial_snapshot IS DISTINCT FROM OLD.component_manufacturer_serial_snapshot OR NEW.notes IS DISTINCT FROM OLD.notes OR NEW.recorded_by <> OLD.recorded_by OR NEW.recorded_at <> OLD.recorded_at OR NEW.recorded_event_id <> OLD.recorded_event_id THEN RAISE EXCEPTION 'Original repair-part fields are immutable'; END IF; IF OLD.voided_by IS NOT NULL OR OLD.voided_at IS NOT NULL OR OLD.void_reason IS NOT NULL OR OLD.void_event_id IS NOT NULL THEN RAISE EXCEPTION 'Voided repair-part history is immutable'; END IF; IF NEW.voided_by IS NULL OR NEW.voided_at IS NULL OR NEW.void_reason IS NULL OR NEW.void_event_id IS NULL THEN RAISE EXCEPTION 'Repair-part void metadata must be written atomically'; END IF; RETURN NEW; END $$; CREATE TRIGGER trg_protect_repair_part_history BEFORE UPDATE OR DELETE ON repair_parts FOR EACH ROW EXECUTE FUNCTION minerops_protect_repair_part_history(); CREATE FUNCTION minerops_validate_repair_part_history() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE recorded repair_events%%ROWTYPE; DECLARE voided repair_events%%ROWTYPE; DECLARE component components%%ROWTYPE; BEGIN IF TG_OP = 'INSERT' AND ( NEW.voided_by IS NOT NULL OR NEW.voided_at IS NOT NULL OR NEW.void_reason IS NOT NULL OR NEW.void_event_id IS NOT NULL) THEN RAISE EXCEPTION 'A repair part must be recorded before it can be voided'; END IF; SELECT * INTO recorded FROM repair_events WHERE id = NEW.recorded_event_id; IF recorded.id IS NULL OR recorded.repair_id <> NEW.repair_id OR recorded.event_type <> 'REPAIR_PART_RECORDED' OR recorded.created_by <> NEW.recorded_by OR recorded.created_at <> NEW.recorded_at OR recorded.payload ->> 'repair_part_id' <> NEW.id::text THEN RAISE EXCEPTION 'Recorded repair-part event does not match its entry'; END IF; IF TG_OP = 'INSERT' AND NEW.component_id IS NOT NULL THEN SELECT * INTO component FROM components WHERE id = NEW.component_id; IF component.id IS NULL OR component.component_type <> NEW.component_type_snapshot OR component.asset_id IS DISTINCT FROM NEW.component_asset_id_snapshot OR component.manufacturer IS DISTINCT FROM NEW.component_manufacturer_snapshot OR component.model IS DISTINCT FROM NEW.component_model_snapshot OR component.manufacturer_serial IS DISTINCT FROM NEW.component_manufacturer_serial_snapshot THEN RAISE EXCEPTION 'Tracked-component snapshot does not match the component'; END IF; END IF; IF NEW.void_event_id IS NOT NULL THEN SELECT * INTO voided FROM repair_events WHERE id = NEW.void_event_id; IF voided.id IS NULL OR voided.repair_id <> NEW.repair_id OR voided.event_type <> 'REPAIR_PART_VOIDED' OR voided.created_by <> NEW.voided_by OR voided.created_at <> NEW.voided_at OR voided.payload ->> 'repair_part_id' <> NEW.id::text OR voided.payload ->> 'reason' <> NEW.void_reason THEN RAISE EXCEPTION 'Voided repair-part event does not match its entry'; END IF; END IF; RETURN NEW; END $$; CREATE TRIGGER trg_validate_repair_part_history BEFORE INSERT OR UPDATE ON repair_parts FOR EACH ROW EXECUTE FUNCTION minerops_validate_repair_part_history(); """ ) def downgrade() -> None: connection = op.get_bind() connection.exec_driver_sql( """ DO $$ DECLARE history_count bigint; BEGIN SELECT count(*) INTO history_count FROM components; history_count := history_count + (SELECT count(*) FROM repair_parts); history_count := history_count + ( SELECT count(*) FROM repair_events WHERE event_type IN ('REPAIR_PART_RECORDED', 'REPAIR_PART_VOIDED') ); IF history_count > 0 THEN RAISE EXCEPTION USING MESSAGE = format( 'Cannot downgrade Repair Parts Tracking: %s component, repair-part, or event records would be destroyed', history_count ), HINT = 'Preserve or explicitly migrate Task 005 identity and history before downgrading; MinerOps will not delete or invent component, actor, timestamp, snapshot, or repair-part history.'; END IF; END $$; """ ) connection.exec_driver_sql( """ DROP TRIGGER trg_validate_repair_part_history ON repair_parts; DROP FUNCTION minerops_validate_repair_part_history(); DROP TRIGGER trg_protect_repair_part_history ON repair_parts; DROP FUNCTION minerops_protect_repair_part_history(); DROP TRIGGER trg_protect_repair_event_history ON repair_events; DROP FUNCTION minerops_protect_repair_event_history(); DROP TRIGGER trg_protect_component_identity ON components; DROP FUNCTION minerops_protect_component_identity(); """ ) op.drop_constraint("ck_repair_events_payload_shape", "repair_events", type_="check") op.drop_constraint("ck_repair_events_type", "repair_events", type_="check") _create_event_constraints(False) op.drop_index("uq_repair_parts_void_event", table_name="repair_parts") op.drop_index("uq_repair_parts_recorded_event", table_name="repair_parts") op.drop_index("idx_repair_parts_voided_by", table_name="repair_parts") op.drop_index("idx_repair_parts_recorded_by", table_name="repair_parts") op.drop_index("idx_repair_parts_component", table_name="repair_parts") op.drop_index("idx_repair_parts_repair", table_name="repair_parts") op.drop_table("repair_parts") op.drop_index("uq_components_normalized_asset_id", table_name="components") op.drop_index("idx_components_type", table_name="components") op.drop_index("idx_components_created_by", table_name="components") op.drop_table("components") connection.exec_driver_sql( """ DELETE FROM role_permissions WHERE permission_id IN ( SELECT id FROM permissions WHERE key IN ('components.read', 'components.create', 'repairs.parts.manage') ); DELETE FROM permissions WHERE key IN ('components.read', 'components.create', 'repairs.parts.manage'); """ )