From 93565cd94cf27a9ae76a411ab527a77a0ae7ee0f Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 2 Jun 2026 16:17:42 -0500 Subject: [PATCH] =?UTF-8?q?ops(db):=20reconcile=20CT=20102=20drift=20?= =?UTF-8?q?=E2=80=94=20stamp=20ledger,=20add=20missing=200002=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../2026-06-02-db-drift-reconcile.sql | 130 ++++++++++++++++++ docs/operations/README.md | 14 ++ 2 files changed, 144 insertions(+) create mode 100644 docs/operations/2026-06-02-db-drift-reconcile.sql create mode 100644 docs/operations/README.md diff --git a/docs/operations/2026-06-02-db-drift-reconcile.sql b/docs/operations/2026-06-02-db-drift-reconcile.sql new file mode 100644 index 0000000..2a2cb07 --- /dev/null +++ b/docs/operations/2026-06-02-db-drift-reconcile.sql @@ -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; diff --git a/docs/operations/README.md b/docs/operations/README.md new file mode 100644 index 0000000..0d4d7bf --- /dev/null +++ b/docs/operations/README.md @@ -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. |