Production data confirms `integration.description` was universally the display NAME (367 rows: avg 13 chars, ~0 real sentences), never a description. - description is now nullable — an actual, optional description distinct from the name. - ONE cloud migration 0005: add name + config_revised_at (+ connection description/tools_synced_at), DROP NOT NULL on description, then backfill name = description (367/367 get a real name, verified none empty) and clear description to NULL so no row carries a duplicated title. name stays column-nullable on purpose: SQLite boot-ensure hosts (self-host, Cloudflare, local) cannot add a NOT NULL column to an existing table, so the read path keeps the name ?? description ?? slug fallback.
Unified · Split
| @@ -1,4 +1,10 @@ | ||
| 1 | +ALTER TABLE "integration" ALTER COLUMN "description" DROP NOT NULL;--> statement-breakpoint | |
| 1 | 2 | ALTER TABLE "connection" ADD COLUMN "description" text;--> statement-breakpoint |
| 2 | 3 | ALTER TABLE "connection" ADD COLUMN "tools_synced_at" bigint;--> statement-breakpoint |
| 3 | 4 | ALTER TABLE "integration" ADD COLUMN "name" text;--> statement-breakpoint |
| 4 | −ALTER TABLE "integration" ADD COLUMN "config_revised_at" bigint; | |
| 5 | +ALTER TABLE "integration" ADD COLUMN "config_revised_at" bigint;--> statement-breakpoint | |
| 6 | +-- Pre-split rows kept the display name in `description`. Move it to `name` | |
| 7 | +-- (its proper home), then clear `description` so it no longer carries a | |
| 8 | +-- duplicated title — it now means an actual, optional description. | |
| 9 | +UPDATE "integration" SET "name" = "description" WHERE "name" IS NULL;--> statement-breakpoint | |
| 10 | +UPDATE "integration" SET "description" = NULL; | |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | − "id": "ab75bf05-6332-4d53-8db4-f131b67773ef", | |
| 2 | + "id": "e105c61d-2fde-4d2e-a785-a74fafd0f4ea", | |
| 3 | 3 | "prevId": "1187b7aa-fdd6-412a-bcfb-20d6ea8f8644", |
| 4 | 4 | "version": "7", |
| 5 | 5 | "dialect": "postgresql", |
| @@ -517,7 +517,7 @@ · 511 unmodified lines | ||
| 517 | 517 | "name": "description", |
| 518 | 518 | "type": "text", |
| 519 | 519 | "primaryKey": false, |
| 520 | − "notNull": true | |
| 520 | + "notNull": false | |
| 521 | 521 | }, |
| 522 | 522 | "config_revised_at": { |
| 523 | 523 | "name": "config_revised_at", |
| @@ -40,7 +40,7 @@ · 39 unmodified lines | ||
| 40 | 40 | { |
| 41 | 41 | "idx": 5, |
| 42 | 42 | "version": "7", |
| 43 | − "when": 1781485927974, | |
| 43 | + "when": 1781491100009, | |
| 44 | 44 | "tag": "0005_integration_descriptions", |
| 45 | 45 | "breakpoints": true |
| 46 | 46 | } |
| @@ -16,7 +16,7 @@ export const integration = pgTable( · 15 unmodified lines | ||
| 16 | 16 | slug: varchar("slug", { length: 255 }).notNull(), |
| 17 | 17 | plugin_id: text("plugin_id").notNull(), |
| 18 | 18 | name: text("name"), |
| 19 | − description: text("description").notNull(), | |
| 19 | + description: text("description"), | |
| 20 | 20 | config_revised_at: bigint("config_revised_at", { mode: "bigint" }), |
| 21 | 21 | config: json("config"), |
| 22 | 22 | can_remove: boolean("can_remove").notNull().default(true), |
| @@ -120,10 +120,17 @@ export const coreTables = defineTables({ · 119 unmodified lines | ||
| 120 | 120 | { |
| 121 | 121 | slug: keyColumn("slug"), |
| 122 | 122 | plugin_id: textColumn("plugin_id"), |
| 123 | − // Display name. Nullable for pre-split rows, where `description` held | |
| 124 | − // the name; readers fall back (see rowToIntegration). | |
| 123 | + // Display name. The pre-split field: `description` used to hold the | |
| 124 | + // name, so cloud backfills `name` from it (migration 0006) and other | |
| 125 | + // hosts fall back at read time (see rowToIntegration). Nullable because | |
| 126 | + // SQLite boot-ensure hosts cannot add a NOT NULL column to existing | |
| 127 | + // tables, so the column stays nullable even though it is always present | |
| 128 | + // in practice. | |
| 125 | 129 | name: nullableTextColumn("name"), |
| 126 | − description: textColumn("description"), | |
| 130 | + // Actual prose description, now distinct from the name. Nullable: absent | |
| 131 | + // until a user/spec supplies one (cloud clears the old duplicated title | |
| 132 | + // to NULL in 0006). | |
| 133 | + description: nullableTextColumn("description"), | |
| 127 | 134 | config: nullableJsonColumn("config"), |
| 128 | 135 | // Epoch ms of the last tool-affecting config change (spec update, auth |
| 129 | 136 | // template edit). Compared against each connection's `tools_synced_at` |
| @@ -449,7 +449,9 @@ const rowToIntegration = ( · 448 unmodified lines | ||
| 449 | 449 | slug: IntegrationSlug.make(row.slug), |
| 450 | 450 | // Pre-split rows have no `name`; their description WAS the display name. |
| 451 | 451 | name: row.name ?? row.description ?? row.slug, |
| 452 | − description: row.description, | |
| 452 | + // `description` is now nullable (cleared where it only held a duplicated | |
| 453 | + // title); present it as "" so the public Integration type stays a string. | |
| 454 | + description: row.description ?? "", | |
| 453 | 455 | kind: row.plugin_id, |
| 454 | 456 | canRemove: Boolean(row.can_remove), |
| 455 | 457 | canRefresh: Boolean(row.can_refresh), |