fix(db): null-safe unique keys on rollup tables, workspace slug unique, link slug scoping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 00:11:39 +02:00
parent 23fd5a5e30
commit 3b19bbe88f
9 changed files with 13 additions and 12 deletions

View File

@ -12,7 +12,7 @@ return new class extends Migration
$table->id();
$table->string('ulid', 26)->unique();
$table->string('name');
$table->string('slug', 64);
$table->string('slug', 64)->unique();
$table->foreignId('owner_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('plan_id')->nullable()->constrained('plans')->nullOnDelete();
$table->timestamp('trial_ends_at')->nullable();

View File

@ -18,6 +18,7 @@ return new class extends Migration
$table->timestamp('expires_at');
$table->timestamp('accepted_at')->nullable();
$table->timestamps();
$table->index(['workspace_id', 'email']);
});
}

View File

@ -29,7 +29,7 @@ return new class extends Migration
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->softDeletes();
$table->timestamps();
$table->unique(['domain_id', 'slug']);
$table->unique(['workspace_id', 'domain_id', 'slug'], 'links_workspace_domain_slug_unique');
$table->index('workspace_id');
$table->index('status');
});

View File

@ -21,7 +21,7 @@ return new class extends Migration
$table->boolean('is_published')->default(false);
$table->softDeletes();
$table->timestamps();
$table->unique(['domain_id', 'slug']);
$table->unique(['workspace_id', 'domain_id', 'slug'], 'bio_pages_workspace_domain_slug_unique');
$table->index('workspace_id');
});
}

View File

@ -12,8 +12,8 @@ return new class extends Migration
$table->id();
$table->foreignId('link_id')->constrained()->cascadeOnDelete();
$table->timestamp('hour');
$table->string('country', 2)->nullable();
$table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->nullable();
$table->string('country', 2)->default('');
$table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->default('unknown');
$table->unsignedBigInteger('count')->default(0);
$table->unsignedBigInteger('unique_visitors')->default(0);
$table->unique(['link_id', 'hour', 'country', 'device']);

View File

@ -12,8 +12,8 @@ return new class extends Migration
$table->id();
$table->foreignId('link_id')->constrained()->cascadeOnDelete();
$table->date('day');
$table->string('country', 2)->nullable();
$table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->nullable();
$table->string('country', 2)->default('');
$table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->default('unknown');
$table->unsignedBigInteger('count')->default(0);
$table->unsignedBigInteger('unique_visitors')->default(0);
$table->unique(['link_id', 'day', 'country', 'device']);

View File

@ -12,7 +12,7 @@ return new class extends Migration
$table->id();
$table->foreignId('link_id')->constrained()->cascadeOnDelete();
$table->date('day');
$table->string('referrer_host', 253)->nullable();
$table->string('referrer_host', 191)->default('');
$table->unsignedBigInteger('count')->default(0);
$table->unique(['link_id', 'day', 'referrer_host']);
$table->index(['link_id', 'day']);

View File

@ -12,9 +12,9 @@ return new class extends Migration
$table->id();
$table->foreignId('link_id')->constrained()->cascadeOnDelete();
$table->date('day');
$table->string('utm_source', 128)->nullable();
$table->string('utm_medium', 128)->nullable();
$table->string('utm_campaign', 128)->nullable();
$table->string('utm_source', 128)->default('');
$table->string('utm_medium', 128)->default('');
$table->string('utm_campaign', 128)->default('');
$table->unsignedBigInteger('count')->default(0);
$table->unique(['link_id', 'day', 'utm_source', 'utm_medium', 'utm_campaign'], 'clicks_by_utm_unique');
$table->index(['link_id', 'day']);

View File

@ -12,7 +12,7 @@ return new class extends Migration
$table->id();
$table->foreignId('webhook_id')->constrained()->cascadeOnDelete();
$table->string('event', 64);
$table->text('payload');
$table->json('payload');
$table->enum('status', ['success', 'failed', 'pending'])->default('pending');
$table->unsignedSmallInteger('response_code')->nullable();
$table->unsignedTinyInteger('attempts')->default(1);