From 2458f87c8dc59b993a613c1527a2bcce203bbd9d Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 8 Feb 2026 21:07:59 +0000 Subject: [PATCH] fix(migrations): make all package migrations idempotent Guard every Schema::create() with hasTable() so migrations coexist safely with the consolidated app-level migration in host.uk.com. Prevents "table already exists" failures that would block the entire migration batch. Co-Authored-By: Virgil --- ...7_004936_create_mcp_api_requests_table.php | 46 ++++---- ...6_000001_create_mcp_tool_metrics_table.php | 56 +++++----- ...6_000002_create_mcp_usage_quotas_table.php | 24 +++-- ..._26_000003_create_mcp_audit_logs_table.php | 102 +++++++++--------- ..._000004_create_mcp_tool_versions_table.php | 44 ++++---- 5 files changed, 143 insertions(+), 129 deletions(-) diff --git a/src/Mcp/Migrations/2026_01_07_004936_create_mcp_api_requests_table.php b/src/Mcp/Migrations/2026_01_07_004936_create_mcp_api_requests_table.php index 76cc9fa..859ab2f 100644 --- a/src/Mcp/Migrations/2026_01_07_004936_create_mcp_api_requests_table.php +++ b/src/Mcp/Migrations/2026_01_07_004936_create_mcp_api_requests_table.php @@ -8,29 +8,31 @@ return new class extends Migration { public function up(): void { - Schema::create('mcp_api_requests', function (Blueprint $table) { - $table->id(); - $table->string('request_id', 32)->unique(); - $table->foreignId('workspace_id')->nullable()->constrained('workspaces')->nullOnDelete(); - $table->foreignId('api_key_id')->nullable()->constrained('api_keys')->nullOnDelete(); - $table->string('method', 10); - $table->string('path', 255); - $table->json('headers')->nullable(); - $table->json('request_body')->nullable(); - $table->unsignedSmallInteger('response_status'); - $table->json('response_body')->nullable(); - $table->unsignedInteger('duration_ms')->default(0); - $table->string('server_id', 64)->nullable(); - $table->string('tool_name', 128)->nullable(); - $table->text('error_message')->nullable(); - $table->string('ip_address', 45)->nullable(); - $table->timestamps(); + if (! Schema::hasTable('mcp_api_requests')) { + Schema::create('mcp_api_requests', function (Blueprint $table) { + $table->id(); + $table->string('request_id', 32)->unique(); + $table->foreignId('workspace_id')->nullable()->constrained('workspaces')->nullOnDelete(); + $table->foreignId('api_key_id')->nullable()->constrained('api_keys')->nullOnDelete(); + $table->string('method', 10); + $table->string('path', 255); + $table->json('headers')->nullable(); + $table->json('request_body')->nullable(); + $table->unsignedSmallInteger('response_status'); + $table->json('response_body')->nullable(); + $table->unsignedInteger('duration_ms')->default(0); + $table->string('server_id', 64)->nullable(); + $table->string('tool_name', 128)->nullable(); + $table->text('error_message')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->timestamps(); - $table->index(['workspace_id', 'created_at']); - $table->index(['server_id', 'tool_name']); - $table->index('created_at'); - $table->index('response_status'); - }); + $table->index(['workspace_id', 'created_at']); + $table->index(['server_id', 'tool_name']); + $table->index('created_at'); + $table->index('response_status'); + }); + } } public function down(): void diff --git a/src/Mcp/Migrations/2026_01_26_000001_create_mcp_tool_metrics_table.php b/src/Mcp/Migrations/2026_01_26_000001_create_mcp_tool_metrics_table.php index d31a179..4d40678 100644 --- a/src/Mcp/Migrations/2026_01_26_000001_create_mcp_tool_metrics_table.php +++ b/src/Mcp/Migrations/2026_01_26_000001_create_mcp_tool_metrics_table.php @@ -8,36 +8,40 @@ return new class extends Migration { public function up(): void { - Schema::create('mcp_tool_metrics', function (Blueprint $table) { - $table->id(); - $table->string('tool_name'); - $table->string('workspace_id')->nullable(); - $table->unsignedInteger('call_count')->default(0); - $table->unsignedInteger('error_count')->default(0); - $table->unsignedInteger('total_duration_ms')->default(0); - $table->unsignedInteger('min_duration_ms')->nullable(); - $table->unsignedInteger('max_duration_ms')->nullable(); - $table->date('date'); - $table->timestamps(); + if (! Schema::hasTable('mcp_tool_metrics')) { + Schema::create('mcp_tool_metrics', function (Blueprint $table) { + $table->id(); + $table->string('tool_name'); + $table->string('workspace_id')->nullable(); + $table->unsignedInteger('call_count')->default(0); + $table->unsignedInteger('error_count')->default(0); + $table->unsignedInteger('total_duration_ms')->default(0); + $table->unsignedInteger('min_duration_ms')->nullable(); + $table->unsignedInteger('max_duration_ms')->nullable(); + $table->date('date'); + $table->timestamps(); - $table->unique(['tool_name', 'workspace_id', 'date']); - $table->index(['date', 'tool_name']); - $table->index('workspace_id'); - }); + $table->unique(['tool_name', 'workspace_id', 'date']); + $table->index(['date', 'tool_name']); + $table->index('workspace_id'); + }); + } // Table for tracking tool combinations (tools used together in sessions) - Schema::create('mcp_tool_combinations', function (Blueprint $table) { - $table->id(); - $table->string('tool_a'); - $table->string('tool_b'); - $table->string('workspace_id')->nullable(); - $table->unsignedInteger('occurrence_count')->default(0); - $table->date('date'); - $table->timestamps(); + if (! Schema::hasTable('mcp_tool_combinations')) { + Schema::create('mcp_tool_combinations', function (Blueprint $table) { + $table->id(); + $table->string('tool_a'); + $table->string('tool_b'); + $table->string('workspace_id')->nullable(); + $table->unsignedInteger('occurrence_count')->default(0); + $table->date('date'); + $table->timestamps(); - $table->unique(['tool_a', 'tool_b', 'workspace_id', 'date']); - $table->index(['date', 'occurrence_count']); - }); + $table->unique(['tool_a', 'tool_b', 'workspace_id', 'date']); + $table->index(['date', 'occurrence_count']); + }); + } } public function down(): void diff --git a/src/Mcp/Migrations/2026_01_26_000002_create_mcp_usage_quotas_table.php b/src/Mcp/Migrations/2026_01_26_000002_create_mcp_usage_quotas_table.php index f3f2180..bfd03b7 100644 --- a/src/Mcp/Migrations/2026_01_26_000002_create_mcp_usage_quotas_table.php +++ b/src/Mcp/Migrations/2026_01_26_000002_create_mcp_usage_quotas_table.php @@ -8,18 +8,20 @@ return new class extends Migration { public function up(): void { - Schema::create('mcp_usage_quotas', function (Blueprint $table) { - $table->id(); - $table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete(); - $table->string('month', 7); // YYYY-MM format - $table->unsignedBigInteger('tool_calls_count')->default(0); - $table->unsignedBigInteger('input_tokens')->default(0); - $table->unsignedBigInteger('output_tokens')->default(0); - $table->timestamps(); + if (! Schema::hasTable('mcp_usage_quotas')) { + Schema::create('mcp_usage_quotas', function (Blueprint $table) { + $table->id(); + $table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete(); + $table->string('month', 7); // YYYY-MM format + $table->unsignedBigInteger('tool_calls_count')->default(0); + $table->unsignedBigInteger('input_tokens')->default(0); + $table->unsignedBigInteger('output_tokens')->default(0); + $table->timestamps(); - $table->unique(['workspace_id', 'month']); - $table->index('month'); - }); + $table->unique(['workspace_id', 'month']); + $table->index('month'); + }); + } } public function down(): void diff --git a/src/Mcp/Migrations/2026_01_26_000003_create_mcp_audit_logs_table.php b/src/Mcp/Migrations/2026_01_26_000003_create_mcp_audit_logs_table.php index 0520748..88ba5b4 100644 --- a/src/Mcp/Migrations/2026_01_26_000003_create_mcp_audit_logs_table.php +++ b/src/Mcp/Migrations/2026_01_26_000003_create_mcp_audit_logs_table.php @@ -8,66 +8,70 @@ return new class extends Migration { public function up(): void { - Schema::create('mcp_audit_logs', function (Blueprint $table) { - $table->id(); + if (! Schema::hasTable('mcp_audit_logs')) { + Schema::create('mcp_audit_logs', function (Blueprint $table) { + $table->id(); - // Tool execution details - $table->string('server_id')->index(); - $table->string('tool_name')->index(); - $table->unsignedBigInteger('workspace_id')->nullable()->index(); - $table->string('session_id')->nullable()->index(); + // Tool execution details + $table->string('server_id')->index(); + $table->string('tool_name')->index(); + $table->unsignedBigInteger('workspace_id')->nullable()->index(); + $table->string('session_id')->nullable()->index(); - // Input/output (stored as JSON, may be redacted) - $table->json('input_params')->nullable(); - $table->json('output_summary')->nullable(); - $table->boolean('success')->default(true); - $table->unsignedInteger('duration_ms')->nullable(); - $table->string('error_code')->nullable(); - $table->text('error_message')->nullable(); + // Input/output (stored as JSON, may be redacted) + $table->json('input_params')->nullable(); + $table->json('output_summary')->nullable(); + $table->boolean('success')->default(true); + $table->unsignedInteger('duration_ms')->nullable(); + $table->string('error_code')->nullable(); + $table->text('error_message')->nullable(); - // Actor information - $table->string('actor_type')->nullable(); // user, api_key, system - $table->unsignedBigInteger('actor_id')->nullable(); - $table->string('actor_ip', 45)->nullable(); // IPv4 or IPv6 + // Actor information + $table->string('actor_type')->nullable(); // user, api_key, system + $table->unsignedBigInteger('actor_id')->nullable(); + $table->string('actor_ip', 45)->nullable(); // IPv4 or IPv6 - // Sensitive tool flagging - $table->boolean('is_sensitive')->default(false)->index(); - $table->string('sensitivity_reason')->nullable(); + // Sensitive tool flagging + $table->boolean('is_sensitive')->default(false)->index(); + $table->string('sensitivity_reason')->nullable(); - // Hash chain for tamper detection - $table->string('previous_hash', 64)->nullable(); // SHA-256 of previous entry - $table->string('entry_hash', 64)->index(); // SHA-256 of this entry + // Hash chain for tamper detection + $table->string('previous_hash', 64)->nullable(); // SHA-256 of previous entry + $table->string('entry_hash', 64)->index(); // SHA-256 of this entry - // Agent context - $table->string('agent_type')->nullable(); - $table->string('plan_slug')->nullable(); + // Agent context + $table->string('agent_type')->nullable(); + $table->string('plan_slug')->nullable(); - // Timestamps (immutable - no updated_at updates after creation) - $table->timestamp('created_at')->useCurrent(); - $table->timestamp('updated_at')->nullable(); + // Timestamps (immutable - no updated_at updates after creation) + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable(); - // Foreign key constraint - $table->foreign('workspace_id') - ->references('id') - ->on('workspaces') - ->nullOnDelete(); + // Foreign key constraint + $table->foreign('workspace_id') + ->references('id') + ->on('workspaces') + ->nullOnDelete(); - // Composite indexes for common queries - $table->index(['workspace_id', 'created_at']); - $table->index(['tool_name', 'created_at']); - $table->index(['is_sensitive', 'created_at']); - $table->index(['actor_type', 'actor_id']); - }); + // Composite indexes for common queries + $table->index(['workspace_id', 'created_at']); + $table->index(['tool_name', 'created_at']); + $table->index(['is_sensitive', 'created_at']); + $table->index(['actor_type', 'actor_id']); + }); + } // Table for tracking sensitive tool definitions - Schema::create('mcp_sensitive_tools', function (Blueprint $table) { - $table->id(); - $table->string('tool_name')->unique(); - $table->string('reason'); - $table->json('redact_fields')->nullable(); // Fields to redact in audit logs - $table->boolean('require_explicit_consent')->default(false); - $table->timestamps(); - }); + if (! Schema::hasTable('mcp_sensitive_tools')) { + Schema::create('mcp_sensitive_tools', function (Blueprint $table) { + $table->id(); + $table->string('tool_name')->unique(); + $table->string('reason'); + $table->json('redact_fields')->nullable(); // Fields to redact in audit logs + $table->boolean('require_explicit_consent')->default(false); + $table->timestamps(); + }); + } } public function down(): void diff --git a/src/Mcp/Migrations/2026_01_26_000004_create_mcp_tool_versions_table.php b/src/Mcp/Migrations/2026_01_26_000004_create_mcp_tool_versions_table.php index 9248f62..04efdcf 100644 --- a/src/Mcp/Migrations/2026_01_26_000004_create_mcp_tool_versions_table.php +++ b/src/Mcp/Migrations/2026_01_26_000004_create_mcp_tool_versions_table.php @@ -8,30 +8,32 @@ return new class extends Migration { public function up(): void { - Schema::create('mcp_tool_versions', function (Blueprint $table) { - $table->id(); - $table->string('server_id', 64)->index(); - $table->string('tool_name', 128); - $table->string('version', 32); // semver: 1.0.0, 2.1.0-beta, etc. - $table->json('input_schema')->nullable(); - $table->json('output_schema')->nullable(); - $table->text('description')->nullable(); - $table->text('changelog')->nullable(); - $table->text('migration_notes')->nullable(); // guidance for upgrading from previous version - $table->boolean('is_latest')->default(false); - $table->timestamp('deprecated_at')->nullable(); - $table->timestamp('sunset_at')->nullable(); // after this date, version is blocked - $table->timestamps(); + if (! Schema::hasTable('mcp_tool_versions')) { + Schema::create('mcp_tool_versions', function (Blueprint $table) { + $table->id(); + $table->string('server_id', 64)->index(); + $table->string('tool_name', 128); + $table->string('version', 32); // semver: 1.0.0, 2.1.0-beta, etc. + $table->json('input_schema')->nullable(); + $table->json('output_schema')->nullable(); + $table->text('description')->nullable(); + $table->text('changelog')->nullable(); + $table->text('migration_notes')->nullable(); // guidance for upgrading from previous version + $table->boolean('is_latest')->default(false); + $table->timestamp('deprecated_at')->nullable(); + $table->timestamp('sunset_at')->nullable(); // after this date, version is blocked + $table->timestamps(); - // Unique constraint: one version per tool per server - $table->unique(['server_id', 'tool_name', 'version'], 'mcp_tool_versions_unique'); + // Unique constraint: one version per tool per server + $table->unique(['server_id', 'tool_name', 'version'], 'mcp_tool_versions_unique'); - // Index for finding latest versions - $table->index(['server_id', 'tool_name', 'is_latest'], 'mcp_tool_versions_latest'); + // Index for finding latest versions + $table->index(['server_id', 'tool_name', 'is_latest'], 'mcp_tool_versions_latest'); - // Index for finding deprecated/sunset versions - $table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle'); - }); + // Index for finding deprecated/sunset versions + $table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle'); + }); + } } public function down(): void