#!/usr/bin/env python3 """ Migration Consolidation Script Consolidates 22 migrations into 13 clean migrations by: 1. Removing items created then dropped (runtime_type_enum, workflow_task_execution table, etc.) 2. Including items added later in initial table creation (is_adhoc, workflow columns, etc.) 3. Moving data insertions to YAML files (runtimes) 4. Consolidating incremental additions (webhook columns, notify triggers) """ import os import re import shutil from pathlib import Path # Base directory BASE_DIR = Path(__file__).parent.parent MIGRATIONS_DIR = BASE_DIR / "migrations" MIGRATIONS_OLD_DIR = BASE_DIR / "migrations.old" def read_migration(filename): """Read a migration file from the old directory.""" path = MIGRATIONS_OLD_DIR / filename if path.exists(): return path.read_text() return None def write_migration(filename, content): """Write a migration file to the new directory.""" path = MIGRATIONS_DIR / filename path.write_text(content) print(f"Created: {filename}") def extract_section(content, start_marker, end_marker=None): """Extract a section of SQL between markers.""" start = content.find(start_marker) if start == -1: return None if end_marker: end = content.find(end_marker, start) if end == -1: end = len(content) else: end = len(content) return content[start:end].strip() def remove_lines_matching(content, patterns): """Remove lines matching any of the patterns.""" lines = content.split("\n") filtered = [] skip_until_semicolon = False for line in lines: # Check if we should skip this line should_skip = False for pattern in patterns: if pattern in line: should_skip = True # If this line doesn't end with semicolon, skip until we find one if ";" not in line: skip_until_semicolon = True break if skip_until_semicolon: if ";" in line: skip_until_semicolon = False continue if not should_skip: filtered.append(line) return "\n".join(filtered) def main(): print("Starting migration consolidation...") print(f"Reading from: {MIGRATIONS_OLD_DIR}") print(f"Writing to: {MIGRATIONS_DIR}") print() # Ensure migrations.old exists if not MIGRATIONS_OLD_DIR.exists(): print("ERROR: migrations.old directory not found!") print("Please run: cp -r migrations migrations.old") return # Clear the migrations directory except README.md for file in MIGRATIONS_DIR.glob("*.sql"): file.unlink() print("Cleared old migrations from migrations/") print() # ======================================================================== # Migration 00001: Initial Setup (modified) # ======================================================================== content_00001 = read_migration("20250101000001_initial_setup.sql") # Remove runtime_type_enum content_00001 = remove_lines_matching( content_00001, [ "-- RuntimeType enum", "CREATE TYPE runtime_type_enum", "COMMENT ON TYPE runtime_type_enum", ], ) # Add worker_role_enum after worker_type_enum worker_role_enum = """ -- WorkerRole enum DO $$ BEGIN CREATE TYPE worker_role_enum AS ENUM ( 'action', 'sensor', 'hybrid' ); EXCEPTION WHEN duplicate_object THEN null; END $$; COMMENT ON TYPE worker_role_enum IS 'Role of worker (action executor, sensor, or both)'; """ # Add pack_environment_status_enum at the end of enums pack_env_enum = """ -- PackEnvironmentStatus enum DO $$ BEGIN CREATE TYPE pack_environment_status_enum AS ENUM ( 'creating', 'ready', 'failed', 'updating', 'deleting' ); EXCEPTION WHEN duplicate_object THEN null; END $$; COMMENT ON TYPE pack_environment_status_enum IS 'Status of pack environment setup'; """ # Insert after worker_type_enum content_00001 = content_00001.replace( "COMMENT ON TYPE worker_type_enum IS 'Type of worker deployment';", "COMMENT ON TYPE worker_type_enum IS 'Type of worker deployment';\n" + worker_role_enum, ) # Insert before SHARED FUNCTIONS content_00001 = content_00001.replace( "-- ============================================================================\n-- SHARED FUNCTIONS", pack_env_enum + "\n-- ============================================================================\n-- SHARED FUNCTIONS", ) write_migration("20250101000001_initial_setup.sql", content_00001) # ======================================================================== # Migration 00002: Identity and Auth # ======================================================================== content_00002 = read_migration("20250101000002_core_tables.sql") # Extract identity, permission, and policy sections identity_section = extract_section( content_00002, "-- IDENTITY TABLE", "-- PERMISSION_SET TABLE" ) permset_section = extract_section( content_00002, "-- PERMISSION_SET TABLE", "-- PERMISSION_ASSIGNMENT TABLE" ) permassign_section = extract_section( content_00002, "-- PERMISSION_ASSIGNMENT TABLE", "-- POLICY TABLE" ) policy_section = extract_section(content_00002, "-- POLICY TABLE", "-- KEY TABLE") migration_00002 = f"""-- Migration: Identity and Authentication -- Description: Creates identity, permission, and policy tables -- Version: 20250101000002 -- ============================================================================ {identity_section} -- ============================================================================ {permset_section} -- ============================================================================ {permassign_section} -- ============================================================================ {policy_section} """ write_migration("20250101000002_identity_and_auth.sql", migration_00002) # ======================================================================== # Migration 00003: Pack System # ======================================================================== pack_section = extract_section(content_00002, "-- PACK TABLE", "-- RUNTIME TABLE") runtime_section = extract_section( content_00002, "-- RUNTIME TABLE", "-- WORKER TABLE" ) # Modify runtime section runtime_section = remove_lines_matching( runtime_section, [ "runtime_type runtime_type_enum NOT NULL,", "runtime_ref_format CHECK (ref ~ '^[^.]+\\.(action|sensor)\\.[^.]+$')", "idx_runtime_type", "idx_runtime_pack_type", "idx_runtime_type_created", ], ) # Add new indexes after idx_runtime_created new_runtime_indexes = """CREATE INDEX idx_runtime_name ON runtime(name); CREATE INDEX idx_runtime_verification ON runtime USING GIN ((distributions->'verification')); """ runtime_section = runtime_section.replace( "CREATE INDEX idx_runtime_created ON runtime(created DESC);", "CREATE INDEX idx_runtime_created ON runtime(created DESC);\n" + new_runtime_indexes, ) # Add pack.installers column in pack table pack_section = pack_section.replace( "is_standard BOOLEAN NOT NULL DEFAULT FALSE,", "is_standard BOOLEAN NOT NULL DEFAULT FALSE,\n installers JSONB DEFAULT '[]'::jsonb,", ) migration_00003 = f"""-- Migration: Pack System -- Description: Creates pack and runtime tables (runtime without runtime_type) -- Version: 20250101000003 -- ============================================================================ {pack_section} -- ============================================================================ {runtime_section} """ write_migration("20250101000003_pack_system.sql", migration_00003) # ======================================================================== # Migration 00004: Action and Sensor # ======================================================================== content_supporting = read_migration("20250101000005_supporting_tables.sql") action_section = extract_section( content_supporting, "-- ACTION TABLE", "-- SENSOR TABLE" ) sensor_section = extract_section( content_supporting, "-- SENSOR TABLE", "-- RULE TABLE" ) # Add is_adhoc to action table action_section = action_section.replace( "enabled BOOLEAN NOT NULL DEFAULT TRUE,", "enabled BOOLEAN NOT NULL DEFAULT TRUE,\n is_adhoc BOOLEAN DEFAULT false NOT NULL,", ) # Add is_adhoc to sensor table sensor_section = sensor_section.replace( "enabled BOOLEAN NOT NULL DEFAULT TRUE,", "enabled BOOLEAN NOT NULL DEFAULT TRUE,\n is_adhoc BOOLEAN DEFAULT false NOT NULL,", ) migration_00004 = f"""-- Migration: Action and Sensor -- Description: Creates action and sensor tables (with is_adhoc from start) -- Version: 20250101000004 -- ============================================================================ {action_section} -- ============================================================================ {sensor_section} -- Add foreign key constraints for policy and key tables ALTER TABLE policy ADD CONSTRAINT policy_action_fkey FOREIGN KEY (action) REFERENCES action(id) ON DELETE CASCADE; ALTER TABLE key ADD CONSTRAINT key_owner_action_fkey FOREIGN KEY (owner_action) REFERENCES action(id) ON DELETE CASCADE; ALTER TABLE key ADD CONSTRAINT key_owner_sensor_fkey FOREIGN KEY (owner_sensor) REFERENCES sensor(id) ON DELETE CASCADE; """ write_migration("20250101000004_action_sensor.sql", migration_00004) # ======================================================================== # Migration 00005: Trigger, Event, and Rule # ======================================================================== content_event = read_migration("20250101000003_event_system.sql") trigger_section = extract_section( content_event, "-- TRIGGER TABLE", "-- SENSOR TABLE" ) event_section = extract_section(content_event, "-- EVENT TABLE", "-- RULE TABLE") rule_section = extract_section( content_event, "-- RULE TABLE", "-- ENFORCEMENT TABLE" ) # Add webhook columns to trigger table trigger_section = trigger_section.replace( "out_schema JSONB,", """out_schema JSONB, webhook_enabled BOOLEAN NOT NULL DEFAULT FALSE, webhook_key VARCHAR(64) UNIQUE, webhook_config JSONB DEFAULT '{}'::jsonb,""", ) # Add webhook index trigger_section = trigger_section.replace( "CREATE INDEX idx_trigger_enabled_created", """CREATE INDEX idx_trigger_webhook_key ON trigger(webhook_key) WHERE webhook_key IS NOT NULL; CREATE INDEX idx_trigger_webhook_enabled_created""", ) # Add rule columns to event table event_section = event_section.replace( "created TIMESTAMPTZ NOT NULL DEFAULT NOW(),", """created TIMESTAMPTZ NOT NULL DEFAULT NOW(), rule BIGINT, rule_ref TEXT,""", ) # Add rule index and constraint to event event_section += """ -- Add foreign key for rule ALTER TABLE event ADD CONSTRAINT event_rule_fkey FOREIGN KEY (rule) REFERENCES rule(id) ON DELETE SET NULL; CREATE INDEX idx_event_rule ON event(rule); """ # Add is_adhoc to rule table rule_section = rule_section.replace( "enabled BOOLEAN NOT NULL DEFAULT TRUE,", "enabled BOOLEAN NOT NULL DEFAULT TRUE,\n is_adhoc BOOLEAN DEFAULT false NOT NULL,", ) migration_00005 = f"""-- Migration: Trigger, Event, and Rule -- Description: Creates trigger (with webhook_config), event (with rule), and rule (with is_adhoc) tables -- Version: 20250101000005 -- ============================================================================ {trigger_section} -- ============================================================================ {event_section} -- ============================================================================ {rule_section} """ write_migration("20250101000005_trigger_event_rule.sql", migration_00005) # ======================================================================== # Migration 00006: Execution System # ======================================================================== content_execution = read_migration("20250101000004_execution_system.sql") enforcement_section = extract_section( content_execution, "-- ENFORCEMENT TABLE", "-- EXECUTION TABLE" ) execution_section = extract_section( content_execution, "-- EXECUTION TABLE", "-- INQUIRY TABLE" ) inquiry_section = extract_section( content_execution, "-- INQUIRY TABLE", "-- WORKFLOW_DEFINITION TABLE" ) # Add workflow columns to execution table execution_section = execution_section.replace( "created TIMESTAMPTZ NOT NULL DEFAULT NOW(),", """created TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_workflow BOOLEAN DEFAULT false NOT NULL, workflow_def BIGINT, workflow_task JSONB,""", ) # Add workflow_def foreign key constraint (will be added after workflow_definition table exists) # For now, just note it in comments migration_00006 = f"""-- Migration: Execution System -- Description: Creates enforcement, execution (with workflow columns), and inquiry tables -- Version: 20250101000006 -- ============================================================================ {enforcement_section} -- ============================================================================ {execution_section} -- ============================================================================ {inquiry_section} -- Add foreign key constraint for enforcement.rule ALTER TABLE enforcement ADD CONSTRAINT enforcement_rule_fkey FOREIGN KEY (rule) REFERENCES rule(id) ON DELETE CASCADE; """ write_migration("20250101000006_execution_system.sql", migration_00006) # ======================================================================== # Migration 00007: Workflow System # ======================================================================== workflow_def_section = extract_section( content_execution, "-- WORKFLOW_DEFINITION TABLE", "-- WORKFLOW_TASK_EXECUTION TABLE", ) migration_00007 = f"""-- Migration: Workflow System -- Description: Creates workflow_definition table (workflow_task_execution consolidated into execution.workflow_task JSONB) -- Version: 20250101000007 -- ============================================================================ {workflow_def_section} -- Add foreign key constraint for execution.workflow_def ALTER TABLE execution ADD CONSTRAINT execution_workflow_def_fkey FOREIGN KEY (workflow_def) REFERENCES workflow_definition(id) ON DELETE CASCADE; """ write_migration("20250101000007_workflow_system.sql", migration_00007) # ======================================================================== # Migration 00008: Worker and Notification # ======================================================================== worker_section = extract_section( content_00002, "-- WORKER TABLE", "-- IDENTITY TABLE" ) notification_section = extract_section( content_supporting, "-- NOTIFICATION TABLE", "-- ARTIFACT TABLE" ) # Add worker_role to worker table worker_section = worker_section.replace( "worker_type worker_type_enum NOT NULL,", """worker_type worker_type_enum NOT NULL, worker_role worker_role_enum NOT NULL DEFAULT 'action',""", ) migration_00008 = f"""-- Migration: Worker and Notification -- Description: Creates worker (with worker_role) and notification tables -- Version: 20250101000008 -- ============================================================================ {worker_section} -- ============================================================================ {notification_section} """ write_migration("20250101000008_worker_notification.sql", migration_00008) # ======================================================================== # Migration 00009: Artifacts and Keys # ======================================================================== artifact_section = extract_section(content_supporting, "-- ARTIFACT TABLE", None) key_section = extract_section(content_00002, "-- KEY TABLE", "-- WORKER TABLE") migration_00009 = f"""-- Migration: Artifacts and Keys -- Description: Creates artifact and key tables for storage and secrets management -- Version: 20250101000009 -- ============================================================================ {artifact_section} -- ============================================================================ {key_section} """ write_migration("20250101000009_artifacts_keys.sql", migration_00009) # ======================================================================== # Migration 00010: Webhook System # ======================================================================== # Get final webhook functions from restore file content_webhook_restore = read_migration( "20260204000001_restore_webhook_functions.sql" ) migration_00010 = ( """-- Migration: Webhook System -- Description: Creates webhook-related functions for trigger activation -- Version: 20250101000010 -- ============================================================================ -- WEBHOOK VALIDATION AND PROCESSING FUNCTIONS -- ============================================================================ """ + content_webhook_restore ) write_migration("20250101000010_webhook_system.sql", migration_00010) # ======================================================================== # Migration 00011: Pack Environments # ======================================================================== content_pack_env = read_migration("20260203000002_add_pack_environments.sql") # Extract pack_environment table section (skip the enum and installers column as they're already added) pack_env_table = extract_section( content_pack_env, "CREATE TABLE pack_environment", None ) migration_00011 = f"""-- Migration: Pack Environments -- Description: Creates pack_environment table for managing pack dependency environments -- Version: 20250101000011 -- ============================================================================ -- PACK_ENVIRONMENT TABLE -- ============================================================================ {pack_env_table} """ write_migration("20250101000011_pack_environments.sql", migration_00011) # ======================================================================== # Migration 00012: Pack Testing # ======================================================================== content_pack_test = read_migration("20260120200000_add_pack_test_results.sql") write_migration("20250101000012_pack_testing.sql", content_pack_test) # ======================================================================== # Migration 00013: LISTEN/NOTIFY Triggers (Consolidated) # ======================================================================== # Read all notify trigger migrations exec_notify = read_migration("20260119000001_add_execution_notify_trigger.sql") event_notify = read_migration("20260129150000_add_event_notify_trigger.sql") rule_trigger_update = read_migration( "20260203000003_add_rule_trigger_to_execution_notify.sql" ) enforcement_notify = read_migration( "20260204000001_add_enforcement_notify_trigger.sql" ) # Get the final version of execution notify (with rule field) exec_notify_final = rule_trigger_update if rule_trigger_update else exec_notify migration_00013 = f"""-- Migration: LISTEN/NOTIFY Triggers -- Description: Consolidated PostgreSQL LISTEN/NOTIFY triggers for real-time events -- Version: 20250101000013 -- ============================================================================ -- EXECUTION CHANGE NOTIFICATION -- ============================================================================ {exec_notify_final} -- ============================================================================ -- EVENT CREATION NOTIFICATION -- ============================================================================ {event_notify} -- ============================================================================ -- ENFORCEMENT CHANGE NOTIFICATION -- ============================================================================ {enforcement_notify} """ write_migration("20250101000013_notify_triggers.sql", migration_00013) print() print("=" * 70) print("Migration consolidation complete!") print("=" * 70) print() print("Summary:") print(f" Old migrations: 22 files") print(f" New migrations: 13 files") print(f" Removed: 9 files (consolidated or data moved to YAML)") print() print("Key changes:") print(" ✓ Removed runtime_type_enum (never recreated)") print( " ✓ Removed workflow_task_execution table (consolidated into execution.workflow_task)" ) print(" ✓ Removed individual webhook columns (consolidated into webhook_config)") print(" ✓ Added is_adhoc flags from start") print(" ✓ Added workflow columns to execution from start") print(" ✓ Added rule tracking to event from start") print(" ✓ Added worker_role from start") print(" ✓ Consolidated all LISTEN/NOTIFY triggers") print() print("Next steps:") print(" 1. Review the generated migrations") print(" 2. Test on fresh database: createdb attune_test && sqlx migrate run") print(" 3. Compare schema: pg_dump --schema-only") print(" 4. If successful, delete migrations.old/") if __name__ == "__main__": main()