ops(db): reconcile CT 102 drift — stamp ledger, add missing 0002 tables
The `tasks` Postgres on CT 102 was bootstrapped years ago with `drizzle-kit push`, which writes schema but skips the ledger. As of today the ledger contained zero rows and the schema was missing `markdown_backlog_items` + `cursor_sync_mappings` (migration 0002 ran on neither code path). Migration 0007's ALTER on markdown_backlog_items had silently failed because of this; audit_log was added manually. The one-shot at docs/operations/2026-06-02-db-drift-reconcile.sql created the two missing tables at the final post-0007 shape (skipping 0002's now-obsolete FK-to-objects intermediate state, which 0003 would have immediately rewritten anyway) and stamped all 8 migrations with their sha256 + journal `when` so drizzle's `lastDbMigration.created_at < migration.folderMillis` gate treats them as already applied. `pnpm db:migrate` is now a clean no-op against this DB. Future migrations work normally. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
336a5890a8
commit
93565cd94c
2 changed files with 144 additions and 0 deletions
130
docs/operations/2026-06-02-db-drift-reconcile.sql
Normal file
130
docs/operations/2026-06-02-db-drift-reconcile.sql
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- One-shot drift reconciliation against CT 102's `tasks` database.
|
||||||
|
--
|
||||||
|
-- Background: the DB was historically bootstrapped with `drizzle-kit push`,
|
||||||
|
-- which mutates the schema directly without recording rows in
|
||||||
|
-- `drizzle.__drizzle_migrations`. As a result, on 2026-06-02 the ledger
|
||||||
|
-- contained ZERO rows even though migrations 0000, 0001, 0003-0006 were
|
||||||
|
-- fully applied and 0007 was partially applied (audit_log existed, but its
|
||||||
|
-- ALTER on markdown_backlog_items had silently failed because that table
|
||||||
|
-- itself was never created — migration 0002 ran on neither code path).
|
||||||
|
--
|
||||||
|
-- This script:
|
||||||
|
-- 1. Creates the two genuinely-missing tables (`markdown_backlog_items`,
|
||||||
|
-- `cursor_sync_mappings`) at their FINAL post-0007 shape — i.e. with
|
||||||
|
-- `markdown_backlog_items.archived_at` already in place and FKs already
|
||||||
|
-- pointing at `workspaces.id` instead of the obsolete mid-0002 target
|
||||||
|
-- of `objects.id`. We skip the bogus intermediate states the migration
|
||||||
|
-- files would otherwise replay because (a) they're broken FKs we'd
|
||||||
|
-- immediately re-flip in 0003 and (b) we're catching up an existing
|
||||||
|
-- DB, not replaying history.
|
||||||
|
-- 2. Inserts one row per migration (0000-0007) into
|
||||||
|
-- `drizzle.__drizzle_migrations` with the SHA-256 hash of each
|
||||||
|
-- migration file and the journal's `when` timestamp. After this,
|
||||||
|
-- drizzle's migrator (which gates on `lastDbMigration.created_at <
|
||||||
|
-- migration.folderMillis`) will treat every existing migration as
|
||||||
|
-- already applied and only run future ones.
|
||||||
|
--
|
||||||
|
-- After running this, `pnpm db:migrate` becomes a clean no-op and future
|
||||||
|
-- migrations work normally.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 1. Missing tables, created at the FINAL schema (post-0007).
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "markdown_backlog_items" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"workspace_id" uuid NOT NULL,
|
||||||
|
"kind" varchar(20) NOT NULL,
|
||||||
|
"slug" varchar(200) NOT NULL,
|
||||||
|
"plan_slug" varchar(200) NOT NULL,
|
||||||
|
"epic_slug" varchar(200),
|
||||||
|
"parent_id" uuid,
|
||||||
|
"repo_path" text NOT NULL,
|
||||||
|
"title" varchar(500) DEFAULT '' NOT NULL,
|
||||||
|
"status" varchar(50),
|
||||||
|
"priority" varchar(20),
|
||||||
|
"owner" text,
|
||||||
|
"frontmatter" jsonb,
|
||||||
|
"body_markdown" text DEFAULT '' NOT NULL,
|
||||||
|
"content_hash" varchar(64) NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
-- From migration 0007 (the ALTER that silently failed because the table
|
||||||
|
-- didn't exist). Now present at create-time.
|
||||||
|
"archived_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "markdown_backlog_items"
|
||||||
|
ADD CONSTRAINT "markdown_backlog_items_workspace_id_workspaces_id_fk"
|
||||||
|
FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
|
||||||
|
ALTER TABLE "markdown_backlog_items"
|
||||||
|
ADD CONSTRAINT "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk"
|
||||||
|
FOREIGN KEY ("parent_id") REFERENCES "public"."markdown_backlog_items"("id")
|
||||||
|
ON DELETE set null ON UPDATE no action;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "markdown_backlog_workspace_repo_path_unique"
|
||||||
|
ON "markdown_backlog_items" USING btree ("workspace_id", "repo_path");
|
||||||
|
CREATE INDEX IF NOT EXISTS "markdown_backlog_workspace_plan_idx"
|
||||||
|
ON "markdown_backlog_items" USING btree ("workspace_id", "plan_slug");
|
||||||
|
CREATE INDEX IF NOT EXISTS "markdown_backlog_parent_id_idx"
|
||||||
|
ON "markdown_backlog_items" USING btree ("parent_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "markdown_backlog_workspace_kind_idx"
|
||||||
|
ON "markdown_backlog_items" USING btree ("workspace_id", "kind");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "cursor_sync_mappings" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"workspace_id" uuid NOT NULL,
|
||||||
|
"backlog_item_id" uuid NOT NULL,
|
||||||
|
"cursor_plan_id" varchar(500),
|
||||||
|
"cursor_item_id" varchar(500),
|
||||||
|
"last_pulled_at" timestamp with time zone,
|
||||||
|
"last_pushed_at" timestamp with time zone,
|
||||||
|
"sync_content_hash" varchar(64),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "cursor_sync_mappings"
|
||||||
|
ADD CONSTRAINT "cursor_sync_mappings_workspace_id_workspaces_id_fk"
|
||||||
|
FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
|
||||||
|
ALTER TABLE "cursor_sync_mappings"
|
||||||
|
ADD CONSTRAINT "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk"
|
||||||
|
FOREIGN KEY ("backlog_item_id") REFERENCES "public"."markdown_backlog_items"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "cursor_sync_mappings_backlog_item_id_unique"
|
||||||
|
ON "cursor_sync_mappings" USING btree ("backlog_item_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "cursor_sync_mappings_workspace_id_idx"
|
||||||
|
ON "cursor_sync_mappings" USING btree ("workspace_id");
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 2. Stamp the drizzle ledger so every existing migration is recorded.
|
||||||
|
--
|
||||||
|
-- (hash, created_at) values come from:
|
||||||
|
-- - hash: shasum -a 256 of the migration file
|
||||||
|
-- - created_at: `when` field in packages/database/migrations/meta/_journal.json
|
||||||
|
--
|
||||||
|
-- Drizzle's migrator only re-runs migrations whose `folderMillis` is
|
||||||
|
-- strictly greater than `MAX(created_at)` in this table, so the values
|
||||||
|
-- have to match the journal to keep the comparison sane.
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO drizzle.__drizzle_migrations (hash, created_at) VALUES
|
||||||
|
('564c54d7eb104751f76ff7e3b96a75b18056df4cab17ff30acdfa06d54d06bc7', 1774617985473),
|
||||||
|
('8519d59dafff5fa86c059baec9678d645edd8d5f3bfaeb2d96d3870fb9c3c180', 1774632231368),
|
||||||
|
('8a10480a136fca4aae8a9a30df1d358bba44cd382c4fcd98a47c337002e2d5f4', 1777225115319),
|
||||||
|
('2251f76466de118a202c348ed6ab42847fe9a3414de6a9c162c228f39fb8febd', 1778124738113),
|
||||||
|
('ef7e1a5a3001528caa33cb3c91aecdebdc1774fbd7c006f1d78433f4a9d220c1', 1779987416637),
|
||||||
|
('535ed60f183650952b512efe994e8b9e81cc7671f5a608af27553abf5f11b30b', 1780412597413),
|
||||||
|
('ca1fa84a89ca42079cdd2bea8bc83062283b7b8f6bb9adc6dfd559100ae7d74b', 1780413875620),
|
||||||
|
('cbdfcf54d2e8fd5093b014f198f925de6b49fc3a77e56d2d3c58a13aadc515f1', 1780424597399);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
14
docs/operations/README.md
Normal file
14
docs/operations/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Operations runbooks
|
||||||
|
|
||||||
|
One-shot scripts and notes for operational fixes that aren't part of the
|
||||||
|
normal app flow. Each file is dated so the chronology is obvious.
|
||||||
|
|
||||||
|
Anything in this folder is **historical**. Don't re-run scripts here
|
||||||
|
without reading them first — most assume a specific broken state and
|
||||||
|
will fail or corrupt a healthy one.
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
| Date | File | What it fixed |
|
||||||
|
|------|------|---------------|
|
||||||
|
| 2026-06-02 | [`2026-06-02-db-drift-reconcile.sql`](./2026-06-02-db-drift-reconcile.sql) | Reconciled CT 102 `tasks` DB after years of `drizzle-kit push` left the migration ledger empty. Created the two tables that had never made it in (`markdown_backlog_items`, `cursor_sync_mappings`) and stamped all 8 migrations as applied. |
|
||||||
Loading…
Reference in a new issue