fix(migration): make all DDL idempotent with IF NOT EXISTS

- 0000: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
  FK constraint wrapped in DO 539994 EXCEPTION WHEN duplicate_object
- 0001: ALTER ADD COLUMN wrapped in DO 539994 EXCEPTION WHEN duplicate_column
- 0002: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS
- 0003: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS
- 0006: ALTER ADD COLUMN wrapped in DO 539994 EXCEPTION,
  DROP COLUMN IF EXISTS

Allows migrations to run cleanly regardless of DB state —
handles fresh DB, partial migration, or wiped __drizzle_migrations.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-12 18:05:09 +07:00
co-authored by Claude
parent fcb199a6ae
commit 33b787dbd6
5 changed files with 68 additions and 58 deletions
@@ -1,4 +1,4 @@
CREATE TABLE "ai_analysis_runs" ( CREATE TABLE IF NOT EXISTS "ai_analysis_runs" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"conversation_key" text NOT NULL, "conversation_key" text NOT NULL,
"target_message_ids" text NOT NULL, "target_message_ids" text NOT NULL,
@@ -11,7 +11,7 @@ CREATE TABLE "ai_analysis_runs" (
"completed_at" bigint "completed_at" bigint
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "attachments" ( CREATE TABLE IF NOT EXISTS "attachments" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"message_id" text NOT NULL, "message_id" text NOT NULL,
"guild_id" text NOT NULL, "guild_id" text NOT NULL,
@@ -29,7 +29,7 @@ CREATE TABLE "attachments" (
"uploaded_at" bigint "uploaded_at" bigint
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "message_reviews" ( CREATE TABLE IF NOT EXISTS "message_reviews" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"message_id" text NOT NULL, "message_id" text NOT NULL,
"guild_id" text NOT NULL, "guild_id" text NOT NULL,
@@ -41,7 +41,7 @@ CREATE TABLE "message_reviews" (
"reviewed_at" bigint "reviewed_at" bigint
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "messages" ( CREATE TABLE IF NOT EXISTS "messages" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"guild_id" text NOT NULL, "guild_id" text NOT NULL,
"channel_id" text NOT NULL, "channel_id" text NOT NULL,
@@ -68,7 +68,7 @@ CREATE TABLE "messages" (
"ai_error" text "ai_error" text
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "moderation_actions" ( CREATE TABLE IF NOT EXISTS "moderation_actions" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"message_id" text, "message_id" text,
"user_id" text, "user_id" text,
@@ -82,7 +82,7 @@ CREATE TABLE "moderation_actions" (
"executed_at" bigint "executed_at" bigint
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "muxer_jobs" ( CREATE TABLE IF NOT EXISTS "muxer_jobs" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"data" text NOT NULL, "data" text NOT NULL,
"status" text DEFAULT 'pending' NOT NULL, "status" text DEFAULT 'pending' NOT NULL,
@@ -93,7 +93,7 @@ CREATE TABLE "muxer_jobs" (
"error" text "error" text
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "retention_policies" ( CREATE TABLE IF NOT EXISTS "retention_policies" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"guild_id" text NOT NULL, "guild_id" text NOT NULL,
"channel_id" text, "channel_id" text,
@@ -105,7 +105,7 @@ CREATE TABLE "retention_policies" (
"updated_at" bigint NOT NULL "updated_at" bigint NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "text_analysis_cache" ( CREATE TABLE IF NOT EXISTS "text_analysis_cache" (
"text" text PRIMARY KEY NOT NULL, "text" text PRIMARY KEY NOT NULL,
"flags" text DEFAULT '[]' NOT NULL, "flags" text DEFAULT '[]' NOT NULL,
"source" text DEFAULT 'local' NOT NULL, "source" text DEFAULT 'local' NOT NULL,
@@ -114,13 +114,13 @@ CREATE TABLE "text_analysis_cache" (
"hit_count" integer DEFAULT 0 NOT NULL "hit_count" integer DEFAULT 0 NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "ui_state" ( CREATE TABLE IF NOT EXISTS "ui_state" (
"key" text PRIMARY KEY NOT NULL, "key" text PRIMARY KEY NOT NULL,
"value" text NOT NULL, "value" text NOT NULL,
"updated_at" bigint NOT NULL "updated_at" bigint NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "voice_recordings" ( CREATE TABLE IF NOT EXISTS "voice_recordings" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL, "user_id" text NOT NULL,
"username" text NOT NULL, "username" text NOT NULL,
@@ -137,40 +137,44 @@ CREATE TABLE "voice_recordings" (
"uploaded_at" bigint "uploaded_at" bigint
); );
--> statement-breakpoint --> statement-breakpoint
ALTER TABLE "attachments" ADD CONSTRAINT "fk_attachments_message_id" FOREIGN KEY ("message_id") REFERENCES "public"."messages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint DO $$ BEGIN
CREATE INDEX "idx_ai_analysis_runs_conversation_key" ON "ai_analysis_runs" USING btree ("conversation_key");--> statement-breakpoint ALTER TABLE "attachments" ADD CONSTRAINT "fk_attachments_message_id" FOREIGN KEY ("message_id") REFERENCES "public"."messages"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "idx_ai_analysis_runs_status" ON "ai_analysis_runs" USING btree ("status");--> statement-breakpoint EXCEPTION WHEN duplicate_object THEN NULL;
CREATE INDEX "idx_ai_analysis_runs_created_at" ON "ai_analysis_runs" USING btree ("created_at");--> statement-breakpoint END $$;
CREATE INDEX "idx_attachments_channel" ON "attachments" USING btree ("channel_id");--> statement-breakpoint --> statement-breakpoint
CREATE INDEX "idx_attachments_message" ON "attachments" USING btree ("message_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_ai_analysis_runs_conversation_key" ON "ai_analysis_runs" USING btree ("conversation_key");--> statement-breakpoint
CREATE INDEX "idx_attachments_status" ON "attachments" USING btree ("upload_status");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_ai_analysis_runs_status" ON "ai_analysis_runs" USING btree ("status");--> statement-breakpoint
CREATE INDEX "idx_attachments_channel_created" ON "attachments" USING btree ("channel_id","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_ai_analysis_runs_created_at" ON "ai_analysis_runs" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_attachments_thread_created" ON "attachments" USING btree ("thread_id","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_attachments_channel" ON "attachments" USING btree ("channel_id");--> statement-breakpoint
CREATE INDEX "idx_message_reviews_message_id" ON "message_reviews" USING btree ("message_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_attachments_message" ON "attachments" USING btree ("message_id");--> statement-breakpoint
CREATE INDEX "idx_message_reviews_status" ON "message_reviews" USING btree ("status");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_attachments_status" ON "attachments" USING btree ("upload_status");--> statement-breakpoint
CREATE INDEX "idx_message_reviews_created_at" ON "message_reviews" USING btree ("created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_attachments_channel_created" ON "attachments" USING btree ("channel_id","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_message_reviews_guild_status" ON "message_reviews" USING btree ("guild_id","status","created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_attachments_thread_created" ON "attachments" USING btree ("thread_id","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_messages_channel" ON "messages" USING btree ("channel_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_message_reviews_message_id" ON "message_reviews" USING btree ("message_id");--> statement-breakpoint
CREATE INDEX "idx_messages_user" ON "messages" USING btree ("user_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_message_reviews_status" ON "message_reviews" USING btree ("status");--> statement-breakpoint
CREATE INDEX "idx_messages_created" ON "messages" USING btree ("created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_message_reviews_created_at" ON "message_reviews" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_messages_thread" ON "messages" USING btree ("thread_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_message_reviews_guild_status" ON "message_reviews" USING btree ("guild_id","status","created_at");--> statement-breakpoint
CREATE INDEX "idx_messages_channel_created" ON "messages" USING btree ("channel_id","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_channel" ON "messages" USING btree ("channel_id");--> statement-breakpoint
CREATE INDEX "idx_messages_thread_created" ON "messages" USING btree ("thread_id","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_user" ON "messages" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_messages_ai_status_created" ON "messages" USING btree ("ai_status","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_created" ON "messages" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_messages_guild_ai_status_created" ON "messages" USING btree ("guild_id","ai_status","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_thread" ON "messages" USING btree ("thread_id");--> statement-breakpoint
CREATE INDEX "idx_messages_guild_created_deleted" ON "messages" USING btree ("guild_id","created_at","deleted_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_channel_created" ON "messages" USING btree ("channel_id","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_messages_channel_ai_status_created" ON "messages" USING btree ("channel_id","ai_status","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_thread_created" ON "messages" USING btree ("thread_id","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_messages_thread_ai_status_created" ON "messages" USING btree ("thread_id","ai_status","created_at","id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_ai_status_created" ON "messages" USING btree ("ai_status","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_moderation_actions_message_id" ON "moderation_actions" USING btree ("message_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_guild_ai_status_created" ON "messages" USING btree ("guild_id","ai_status","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_moderation_actions_user_id" ON "moderation_actions" USING btree ("user_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_guild_created_deleted" ON "messages" USING btree ("guild_id","created_at","deleted_at","id");--> statement-breakpoint
CREATE INDEX "idx_moderation_actions_status" ON "moderation_actions" USING btree ("status");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_channel_ai_status_created" ON "messages" USING btree ("channel_id","ai_status","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_moderation_actions_guild_status" ON "moderation_actions" USING btree ("guild_id","status","created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_messages_thread_ai_status_created" ON "messages" USING btree ("thread_id","ai_status","created_at","id");--> statement-breakpoint
CREATE INDEX "idx_muxer_jobs_status" ON "muxer_jobs" USING btree ("status");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_moderation_actions_message_id" ON "moderation_actions" USING btree ("message_id");--> statement-breakpoint
CREATE INDEX "idx_muxer_jobs_createdAt" ON "muxer_jobs" USING btree ("createdAt");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_moderation_actions_user_id" ON "moderation_actions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_retention_policies_guild_id" ON "retention_policies" USING btree ("guild_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_moderation_actions_status" ON "moderation_actions" USING btree ("status");--> statement-breakpoint
CREATE INDEX "idx_retention_policies_enabled" ON "retention_policies" USING btree ("enabled");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_moderation_actions_guild_status" ON "moderation_actions" USING btree ("guild_id","status","created_at");--> statement-breakpoint
CREATE INDEX "idx_text_analysis_cache_expires_at" ON "text_analysis_cache" USING btree ("expires_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_muxer_jobs_status" ON "muxer_jobs" USING btree ("status");--> statement-breakpoint
CREATE INDEX "idx_text_analysis_cache_source" ON "text_analysis_cache" USING btree ("source");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_muxer_jobs_createdAt" ON "muxer_jobs" USING btree ("createdAt");--> statement-breakpoint
CREATE INDEX "idx_voice_recordings_user_id" ON "voice_recordings" USING btree ("user_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_retention_policies_guild_id" ON "retention_policies" USING btree ("guild_id");--> statement-breakpoint
CREATE INDEX "idx_voice_recordings_channel_id" ON "voice_recordings" USING btree ("channel_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_retention_policies_enabled" ON "retention_policies" USING btree ("enabled");--> statement-breakpoint
CREATE INDEX "idx_voice_recordings_created_at" ON "voice_recordings" USING btree ("created_at"); CREATE INDEX IF NOT EXISTS "idx_text_analysis_cache_expires_at" ON "text_analysis_cache" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_text_analysis_cache_source" ON "text_analysis_cache" USING btree ("source");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_voice_recordings_user_id" ON "voice_recordings" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_voice_recordings_channel_id" ON "voice_recordings" USING btree ("channel_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_voice_recordings_created_at" ON "voice_recordings" USING btree ("created_at");
@@ -1,6 +1,9 @@
-- Add model_version column to text_analysis_cache table -- Add model_version column to text_analysis_cache table
ALTER TABLE "text_analysis_cache" ADD COLUMN "model_version" text DEFAULT 'v1' NOT NULL; DO $$ BEGIN
ALTER TABLE "text_analysis_cache" ADD COLUMN "model_version" text DEFAULT 'v1' NOT NULL;
EXCEPTION WHEN duplicate_column THEN NULL;
END $$;
-- Create indexes for model_version and composite source+model_version -- Create indexes for model_version and composite source+model_version
CREATE INDEX "idx_text_analysis_cache_model_version" ON "text_analysis_cache" USING btree ("model_version"); CREATE INDEX IF NOT EXISTS "idx_text_analysis_cache_model_version" ON "text_analysis_cache" USING btree ("model_version");
CREATE INDEX "idx_text_analysis_cache_source_model_version" ON "text_analysis_cache" USING btree ("source","model_version"); CREATE INDEX IF NOT EXISTS "idx_text_analysis_cache_source_model_version" ON "text_analysis_cache" USING btree ("source","model_version");
@@ -1,4 +1,4 @@
CREATE TABLE "sticker_cache" ( CREATE TABLE IF NOT EXISTS "sticker_cache" (
"name" text PRIMARY KEY NOT NULL, "name" text PRIMARY KEY NOT NULL,
"base64" text NOT NULL, "base64" text NOT NULL,
"mime_type" text NOT NULL, "mime_type" text NOT NULL,
@@ -6,4 +6,4 @@ CREATE TABLE "sticker_cache" (
"fetched_at" bigint NOT NULL "fetched_at" bigint NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX "idx_sticker_cache_fetched_at" ON "sticker_cache" USING btree ("fetched_at"); CREATE INDEX IF NOT EXISTS "idx_sticker_cache_fetched_at" ON "sticker_cache" USING btree ("fetched_at");
@@ -1,4 +1,4 @@
CREATE TABLE "corrected_moderations" ( CREATE TABLE IF NOT EXISTS "corrected_moderations" (
"id" text PRIMARY KEY NOT NULL, "id" text PRIMARY KEY NOT NULL,
"message_id" text NOT NULL, "message_id" text NOT NULL,
"original_flags" text NOT NULL, "original_flags" text NOT NULL,
@@ -8,5 +8,5 @@ CREATE TABLE "corrected_moderations" (
"created_at" bigint NOT NULL "created_at" bigint NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX "idx_corrected_moderations_created_at" ON "corrected_moderations" USING btree ("created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "idx_corrected_moderations_created_at" ON "corrected_moderations" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_corrected_moderations_message_id" ON "corrected_moderations" USING btree ("message_id"); CREATE INDEX IF NOT EXISTS "idx_corrected_moderations_message_id" ON "corrected_moderations" USING btree ("message_id");
@@ -1,7 +1,10 @@
-- Replace base64 blob storage with uploaded image URL -- Replace base64 blob storage with uploaded image URL
-- Old base64 data is dropped; cache will refill on next occurrence via upload+URL. -- Old base64 data is dropped; cache will refill on next occurrence via upload+URL.
ALTER TABLE "sticker_cache" ADD COLUMN "image_url" text NOT NULL DEFAULT ''; DO $$ BEGIN
ALTER TABLE "sticker_cache" ADD COLUMN "image_url" text NOT NULL DEFAULT '';
EXCEPTION WHEN duplicate_column THEN NULL;
END $$;
--> statement-breakpoint --> statement-breakpoint
ALTER TABLE "sticker_cache" DROP COLUMN "base64"; ALTER TABLE "sticker_cache" DROP COLUMN IF EXISTS "base64";
--> statement-breakpoint --> statement-breakpoint
ALTER TABLE "sticker_cache" DROP COLUMN "size"; ALTER TABLE "sticker_cache" DROP COLUMN IF EXISTS "size";