From ae969f93e73326976db7f224b6768019f34970e4 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 25 Jun 2026 09:11:55 +0100 Subject: [PATCH] fix(dashboard-agent-db): isolate the migration journal table Drizzle's default journal (drizzle.__drizzle_migrations) is shared by every Drizzle app, so when this database is shared with another, the other app's history can make the migrator skip our CREATE SCHEMA and fail a later migration against a schema that was never created. Track our migrations in a dedicated __dashboard_agent_migrations table, and make the first migrations idempotent so existing databases re-run cleanly after the rename. --- .../dashboard-agent-db/drizzle.config.ts | 4 ++++ .../drizzle/0000_magenta_lilandra.sql | 8 +++---- .../drizzle/0001_slimy_living_tribunal.sql | 6 ++--- .../dashboard-agent-db/migrate.mjs | 24 +++++++++++++------ 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/internal-packages/dashboard-agent-db/drizzle.config.ts b/internal-packages/dashboard-agent-db/drizzle.config.ts index 6c057cfdad..ddaddb9fd8 100644 --- a/internal-packages/dashboard-agent-db/drizzle.config.ts +++ b/internal-packages/dashboard-agent-db/drizzle.config.ts @@ -13,5 +13,9 @@ export default defineConfig({ dialect: "postgresql", // Only manage our schema — never introspect or diff Prisma's `public` schema. schemaFilter: ["trigger_dashboard_agent"], + // Own journal table so dev (`drizzle-kit migrate`) and deploy (migrate.mjs) + // share one history and we don't cross-poison the default + // drizzle.__drizzle_migrations when the DB is shared. See migrate.mjs. + migrations: { table: "__dashboard_agent_migrations", schema: "drizzle" }, dbCredentials: { url }, }); diff --git a/internal-packages/dashboard-agent-db/drizzle/0000_magenta_lilandra.sql b/internal-packages/dashboard-agent-db/drizzle/0000_magenta_lilandra.sql index 2429b1f29d..0eb2c72b98 100644 --- a/internal-packages/dashboard-agent-db/drizzle/0000_magenta_lilandra.sql +++ b/internal-packages/dashboard-agent-db/drizzle/0000_magenta_lilandra.sql @@ -1,6 +1,6 @@ -CREATE SCHEMA "trigger_dashboard_agent"; +CREATE SCHEMA IF NOT EXISTS "trigger_dashboard_agent"; --> statement-breakpoint -CREATE TABLE "trigger_dashboard_agent"."chat_sessions" ( +CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chat_sessions" ( "chat_id" text PRIMARY KEY NOT NULL, "public_access_token" text NOT NULL, "last_event_id" text, @@ -8,7 +8,7 @@ CREATE TABLE "trigger_dashboard_agent"."chat_sessions" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE TABLE "trigger_dashboard_agent"."chats" ( +CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chats" ( "id" text PRIMARY KEY NOT NULL, "organization_id" text NOT NULL, "user_id" text NOT NULL, @@ -22,4 +22,4 @@ CREATE TABLE "trigger_dashboard_agent"."chats" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE INDEX "chats_org_user_last_msg_idx" ON "trigger_dashboard_agent"."chats" USING btree ("organization_id","user_id","last_message_at" DESC NULLS LAST) WHERE "trigger_dashboard_agent"."chats"."deleted_at" is null; \ No newline at end of file +CREATE INDEX IF NOT EXISTS "chats_org_user_last_msg_idx" ON "trigger_dashboard_agent"."chats" USING btree ("organization_id","user_id","last_message_at" DESC NULLS LAST) WHERE "trigger_dashboard_agent"."chats"."deleted_at" is null; \ No newline at end of file diff --git a/internal-packages/dashboard-agent-db/drizzle/0001_slimy_living_tribunal.sql b/internal-packages/dashboard-agent-db/drizzle/0001_slimy_living_tribunal.sql index fe696d8138..4c9afe5f6c 100644 --- a/internal-packages/dashboard-agent-db/drizzle/0001_slimy_living_tribunal.sql +++ b/internal-packages/dashboard-agent-db/drizzle/0001_slimy_living_tribunal.sql @@ -1,4 +1,4 @@ -CREATE TABLE "trigger_dashboard_agent"."chat_turn_evals" ( +CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chat_turn_evals" ( "chat_id" text NOT NULL, "turn" integer NOT NULL, "organization_id" text NOT NULL, @@ -34,5 +34,5 @@ CREATE TABLE "trigger_dashboard_agent"."chat_turn_evals" ( CONSTRAINT "chat_turn_evals_chat_id_turn_pk" PRIMARY KEY("chat_id","turn") ); --> statement-breakpoint -CREATE INDEX "chat_turn_evals_org_created_idx" ON "trigger_dashboard_agent"."chat_turn_evals" USING btree ("organization_id","created_at" DESC NULLS LAST);--> statement-breakpoint -CREATE INDEX "chat_turn_evals_org_opps_idx" ON "trigger_dashboard_agent"."chat_turn_evals" USING btree ("organization_id","created_at" DESC NULLS LAST) WHERE "trigger_dashboard_agent"."chat_turn_evals"."capability_gap" or "trigger_dashboard_agent"."chat_turn_evals"."docs_gap" or "trigger_dashboard_agent"."chat_turn_evals"."support_opportunity" or "trigger_dashboard_agent"."chat_turn_evals"."feature_request"; \ No newline at end of file +CREATE INDEX IF NOT EXISTS "chat_turn_evals_org_created_idx" ON "trigger_dashboard_agent"."chat_turn_evals" USING btree ("organization_id","created_at" DESC NULLS LAST);--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "chat_turn_evals_org_opps_idx" ON "trigger_dashboard_agent"."chat_turn_evals" USING btree ("organization_id","created_at" DESC NULLS LAST) WHERE "trigger_dashboard_agent"."chat_turn_evals"."capability_gap" or "trigger_dashboard_agent"."chat_turn_evals"."docs_gap" or "trigger_dashboard_agent"."chat_turn_evals"."support_opportunity" or "trigger_dashboard_agent"."chat_turn_evals"."feature_request"; \ No newline at end of file diff --git a/internal-packages/dashboard-agent-db/migrate.mjs b/internal-packages/dashboard-agent-db/migrate.mjs index 48699b23cc..d840707ddd 100644 --- a/internal-packages/dashboard-agent-db/migrate.mjs +++ b/internal-packages/dashboard-agent-db/migrate.mjs @@ -44,13 +44,23 @@ const sql = postgres(normalizeConnectionString(connectionString), { }); try { - // Journal lives in Drizzle's default `drizzle` schema (matching `drizzle-kit - // migrate`, so dev and deploy track migrations the same way). It must not be - // our data schema: the first migration runs `CREATE SCHEMA - // "trigger_dashboard_agent"`, which would collide with the journal schema the - // migrator pre-creates. The dashboard agent is the only Drizzle user of its - // database, so the `drizzle` schema stays exclusively ours. - await migrate(drizzle(sql), { migrationsFolder }); + // Track our history in a DEDICATED journal table. Drizzle's migrator reads the + // latest row from . by created_at and skips any journal entry + // dated at or before it. The default `drizzle.__drizzle_migrations` is shared + // by every Drizzle app, so when this DB is shared (OSS single-database fallback + // and the enterprise-image E2E gate) billing's journal rows poison ours: a + // billing row dated between our 0000 and 0001 makes the migrator skip 0000 (the + // CREATE SCHEMA) and run 0001 against a schema that never got created. An own + // table keeps our history independent (enterprise/db does the same with + // __enterprise_migrations; billing keeps the default). + // + // The table stays in the `drizzle` schema, not our data schema, so 0000's + // `CREATE SCHEMA "trigger_dashboard_agent"` doesn't collide with the schema the + // migrator pre-creates for its journal. + await migrate(drizzle(sql), { + migrationsFolder, + migrationsTable: "__dashboard_agent_migrations", + }); console.log("[dashboard-agent-db] migrations complete"); } finally { await sql.end();