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 | ALTER TABLE "connection" ADD COLUMN "description" text;--> statement-breakpoint | 2 | ALTER TABLE "connection" ADD COLUMN "description" text;--> statement-breakpoint |
| 2 | ALTER TABLE "connection" ADD COLUMN "tools_synced_at" bigint;--> statement-breakpoint | 3 | ALTER TABLE "connection" ADD COLUMN "tools_synced_at" bigint;--> statement-breakpoint |
| 3 | ALTER TABLE "integration" ADD COLUMN "name" text;--> statement-breakpoint | 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 | "prevId": "1187b7aa-fdd6-412a-bcfb-20d6ea8f8644", | 3 | "prevId": "1187b7aa-fdd6-412a-bcfb-20d6ea8f8644", |
| 4 | "version": "7", | 4 | "version": "7", |
| 5 | "dialect": "postgresql", | 5 | "dialect": "postgresql", |
| @@ -517,7 +517,7 @@ · 511 unmodified lines | |||
| 517 | "name": "description", | 517 | "name": "description", |
| 518 | "type": "text", | 518 | "type": "text", |
| 519 | "primaryKey": false, | 519 | "primaryKey": false, |
| 520 | "notNull": true | 520 | "notNull": false |
| 521 | }, | 521 | }, |
| 522 | "config_revised_at": { | 522 | "config_revised_at": { |
| 523 | "name": "config_revised_at", | 523 | "name": "config_revised_at", |
| @@ -40,7 +40,7 @@ · 39 unmodified lines | |||
| 40 | { | 40 | { |
| 41 | "idx": 5, | 41 | "idx": 5, |
| 42 | "version": "7", | 42 | "version": "7", |
| 43 | "when": 1781485927974, | 43 | "when": 1781491100009, |
| 44 | "tag": "0005_integration_descriptions", | 44 | "tag": "0005_integration_descriptions", |
| 45 | "breakpoints": true | 45 | "breakpoints": true |
| 46 | } | 46 | } |
| @@ -16,7 +16,7 @@ export const integration = pgTable( · 15 unmodified lines | |||
| 16 | slug: varchar("slug", { length: 255 }).notNull(), | 16 | slug: varchar("slug", { length: 255 }).notNull(), |
| 17 | plugin_id: text("plugin_id").notNull(), | 17 | plugin_id: text("plugin_id").notNull(), |
| 18 | name: text("name"), | 18 | name: text("name"), |
| 19 | description: text("description").notNull(), | 19 | description: text("description"), |
| 20 | config_revised_at: bigint("config_revised_at", { mode: "bigint" }), | 20 | config_revised_at: bigint("config_revised_at", { mode: "bigint" }), |
| 21 | config: json("config"), | 21 | config: json("config"), |
| 22 | can_remove: boolean("can_remove").notNull().default(true), | 22 | can_remove: boolean("can_remove").notNull().default(true), |
| @@ -120,10 +120,17 @@ export const coreTables = defineTables({ · 119 unmodified lines | |||
| 120 | { | 120 | { |
| 121 | slug: keyColumn("slug"), | 121 | slug: keyColumn("slug"), |
| 122 | plugin_id: textColumn("plugin_id"), | 122 | plugin_id: textColumn("plugin_id"), |
| 123 | // Display name. Nullable for pre-split rows, where `description` held | 123 | // Display name. The pre-split field: `description` used to hold the |
| 124 | // the name; readers fall back (see rowToIntegration). | 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 | name: nullableTextColumn("name"), | 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 | config: nullableJsonColumn("config"), | 134 | config: nullableJsonColumn("config"), |
| 128 | // Epoch ms of the last tool-affecting config change (spec update, auth | 135 | // Epoch ms of the last tool-affecting config change (spec update, auth |
| 129 | // template edit). Compared against each connection's `tools_synced_at` | 136 | // template edit). Compared against each connection's `tools_synced_at` |
| @@ -449,7 +449,9 @@ const rowToIntegration = ( · 448 unmodified lines | |||
| 449 | slug: IntegrationSlug.make(row.slug), | 449 | slug: IntegrationSlug.make(row.slug), |
| 450 | // Pre-split rows have no `name`; their description WAS the display name. | 450 | // Pre-split rows have no `name`; their description WAS the display name. |
| 451 | name: row.name ?? row.description ?? row.slug, | 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 | kind: row.plugin_id, | 455 | kind: row.plugin_id, |
| 454 | canRemove: Boolean(row.can_remove), | 456 | canRemove: Boolean(row.can_remove), |
| 455 | canRefresh: Boolean(row.can_refresh), | 457 | canRefresh: Boolean(row.can_refresh), |