Skip to content

Commit 82bdd1f

Browse files
authored
fix(dashboard-agent-db): isolate the migration journal table (#4032)
## Summary The dashboard agent's database migrations could be silently skipped when its database is shared with another Drizzle application, leaving the `trigger_dashboard_agent` schema uncreated and a later migration failing with `schema "trigger_dashboard_agent" does not exist`. ## Root cause Drizzle's migrator decides what to run by reading the most recent row from its journal table by `created_at`, and skipping any migration dated at or before it. The dashboard-agent runner used Drizzle's default journal table, `drizzle.__drizzle_migrations`, which every Drizzle app shares by default. When the database is shared, another app's journal row dated between two of our migrations makes the migrator treat the earlier one (the `CREATE SCHEMA`) as already applied and run a later one against a schema that was never created. ## Fix - Track the dashboard-agent migrations in a dedicated journal table (`drizzle.__dashboard_agent_migrations`), in both the deploy runner (`migrate.mjs`) and the drizzle-kit config, so its history is independent of any other Drizzle app sharing the database. The table stays in the `drizzle` schema so the first migration's `CREATE SCHEMA "trigger_dashboard_agent"` does not collide with it. - Make the first two migrations idempotent (`CREATE SCHEMA/TABLE/INDEX IF NOT EXISTS`) so databases that already tracked them under the old journal table re-run cleanly after the rename instead of erroring on the bare `CREATE SCHEMA`. Verified against a Postgres seeded to reproduce the skip: the old default-table path fails as above, the dedicated-table path creates the schema and all tables, and re-running on an already-migrated database is a clean no-op.
1 parent 422d8da commit 82bdd1f

4 files changed

Lines changed: 28 additions & 14 deletions

File tree

internal-packages/dashboard-agent-db/drizzle.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ export default defineConfig({
1313
dialect: "postgresql",
1414
// Only manage our schema — never introspect or diff Prisma's `public` schema.
1515
schemaFilter: ["trigger_dashboard_agent"],
16+
// Own journal table so dev (`drizzle-kit migrate`) and deploy (migrate.mjs)
17+
// share one history and we don't cross-poison the default
18+
// drizzle.__drizzle_migrations when the DB is shared. See migrate.mjs.
19+
migrations: { table: "__dashboard_agent_migrations", schema: "drizzle" },
1620
dbCredentials: { url },
1721
});

internal-packages/dashboard-agent-db/drizzle/0000_magenta_lilandra.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
CREATE SCHEMA "trigger_dashboard_agent";
1+
CREATE SCHEMA IF NOT EXISTS "trigger_dashboard_agent";
22
--> statement-breakpoint
3-
CREATE TABLE "trigger_dashboard_agent"."chat_sessions" (
3+
CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chat_sessions" (
44
"chat_id" text PRIMARY KEY NOT NULL,
55
"public_access_token" text NOT NULL,
66
"last_event_id" text,
77
"run_id" text,
88
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
99
);
1010
--> statement-breakpoint
11-
CREATE TABLE "trigger_dashboard_agent"."chats" (
11+
CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chats" (
1212
"id" text PRIMARY KEY NOT NULL,
1313
"organization_id" text NOT NULL,
1414
"user_id" text NOT NULL,
@@ -22,4 +22,4 @@ CREATE TABLE "trigger_dashboard_agent"."chats" (
2222
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
2323
);
2424
--> statement-breakpoint
25-
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;
25+
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;

internal-packages/dashboard-agent-db/drizzle/0001_slimy_living_tribunal.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CREATE TABLE "trigger_dashboard_agent"."chat_turn_evals" (
1+
CREATE TABLE IF NOT EXISTS "trigger_dashboard_agent"."chat_turn_evals" (
22
"chat_id" text NOT NULL,
33
"turn" integer NOT NULL,
44
"organization_id" text NOT NULL,
@@ -34,5 +34,5 @@ CREATE TABLE "trigger_dashboard_agent"."chat_turn_evals" (
3434
CONSTRAINT "chat_turn_evals_chat_id_turn_pk" PRIMARY KEY("chat_id","turn")
3535
);
3636
--> statement-breakpoint
37-
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
38-
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";
37+
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
38+
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";

internal-packages/dashboard-agent-db/migrate.mjs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,23 @@ const sql = postgres(normalizeConnectionString(connectionString), {
4444
});
4545

4646
try {
47-
// Journal lives in Drizzle's default `drizzle` schema (matching `drizzle-kit
48-
// migrate`, so dev and deploy track migrations the same way). It must not be
49-
// our data schema: the first migration runs `CREATE SCHEMA
50-
// "trigger_dashboard_agent"`, which would collide with the journal schema the
51-
// migrator pre-creates. The dashboard agent is the only Drizzle user of its
52-
// database, so the `drizzle` schema stays exclusively ours.
53-
await migrate(drizzle(sql), { migrationsFolder });
47+
// Track our history in a DEDICATED journal table. Drizzle's migrator reads the
48+
// latest row from <schema>.<table> by created_at and skips any journal entry
49+
// dated at or before it. The default `drizzle.__drizzle_migrations` is shared
50+
// by every Drizzle app, so when this DB is shared (OSS single-database fallback
51+
// and the enterprise-image E2E gate) billing's journal rows poison ours: a
52+
// billing row dated between our 0000 and 0001 makes the migrator skip 0000 (the
53+
// CREATE SCHEMA) and run 0001 against a schema that never got created. An own
54+
// table keeps our history independent (enterprise/db does the same with
55+
// __enterprise_migrations; billing keeps the default).
56+
//
57+
// The table stays in the `drizzle` schema, not our data schema, so 0000's
58+
// `CREATE SCHEMA "trigger_dashboard_agent"` doesn't collide with the schema the
59+
// migrator pre-creates for its journal.
60+
await migrate(drizzle(sql), {
61+
migrationsFolder,
62+
migrationsTable: "__dashboard_agent_migrations",
63+
});
5464
console.log("[dashboard-agent-db] migrations complete");
5565
} finally {
5666
await sql.end();

0 commit comments

Comments
 (0)