diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 9c8b22bc3..44307a32c 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -55,6 +55,7 @@ import Migration0040 from "./Migrations/040_ProjectionThreadsPinOrderKey.ts"; import Migration0041 from "./Migrations/041_ProjectionProjectsDefaultThreadEnvMode.ts"; import Migration0042 from "./Migrations/042_ProjectionProjectFaviconPath.ts"; import Migration0043 from "./Migrations/043_ProjectionThreadSessionLifecycle.ts"; +import Migration0044 from "./Migrations/044_RepairProjectsDefaultThreadEnvMode.ts"; /** * Migration loader with all migrations defined inline. * @@ -119,6 +120,10 @@ export const migrationEntries = [ // (pingdotgg/t3code#5775). [42, "ProjectionProjectFaviconPath", Migration0042], [43, "ProjectionThreadSessionLifecycle", Migration0043], + // Session lifecycle also shipped as 41 in an earlier build. Those databases + // record 41 as their high water mark, so they skip 41 above and never gain + // `default_thread_env_mode`. This re-adds it from above the mark. + [44, "RepairProjectsDefaultThreadEnvMode", Migration0044], ] as const; export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); diff --git a/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts new file mode 100644 index 000000000..f7b02fe44 --- /dev/null +++ b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.test.ts @@ -0,0 +1,56 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +const columnNames = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_projects) + `; + return columns.map((column) => column.name); +}); + +layer("044_RepairProjectsDefaultThreadEnvMode", (it) => { + it.effect("restores the column on a database that skipped 041", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + // Reproduce a build that shipped session lifecycle as 41: stop before + // 041, then claim 41 so the runner treats everything at or below it as + // done. 042 and 043 still run; 041 never does. + yield* runMigrations({ toMigrationInclusive: 40 }); + yield* sql` + INSERT INTO effect_sql_migrations (migration_id, name, created_at) + VALUES (41, 'ProjectionThreadSessionLifecycle', CURRENT_TIMESTAMP) + `; + yield* runMigrations({ toMigrationInclusive: 43 }); + + assert.isFalse( + (yield* columnNames).includes("default_thread_env_mode"), + "expected the skip this migration exists to repair", + ); + + yield* runMigrations({ toMigrationInclusive: 44 }); + + assert.isTrue((yield* columnNames).includes("default_thread_env_mode")); + }), + ); + + it.effect("leaves a correctly migrated database alone", () => + Effect.gen(function* () { + yield* runMigrations({ toMigrationInclusive: 43 }); + const before = yield* columnNames; + assert.isTrue(before.includes("default_thread_env_mode")); + + yield* runMigrations({ toMigrationInclusive: 44 }); + + assert.deepStrictEqual(yield* columnNames, before); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts new file mode 100644 index 000000000..0b2841286 --- /dev/null +++ b/apps/server/src/persistence/Migrations/044_RepairProjectsDefaultThreadEnvMode.ts @@ -0,0 +1,31 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +/** + * Repairs installs that skipped migration 041. + * + * Some builds shipped `ProjectionThreadSessionLifecycle` as id 41 before this + * branch settled on 041 = `ProjectionProjectsDefaultThreadEnvMode`, 042 = + * `ProjectionProjectFaviconPath`, 043 = session lifecycle. The runner only + * applies ids above the highest one recorded, so a database written by such a + * build reports 41 as done, runs 042 and 043, and never runs 041 — leaving + * `projection_projects` without `default_thread_env_mode`. Every project query + * then fails with `no such column`, which takes the server down at startup. + * + * Renumbering after release is what caused this, so the fix cannot be another + * renumber: those databases would still skip whatever sits below their high + * water mark. A new migration above it is the only thing they will run. + */ +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_projects) + `; + + if (!columns.some((column) => column.name === "default_thread_env_mode")) { + yield* sql` + ALTER TABLE projection_projects + ADD COLUMN default_thread_env_mode TEXT + `; + } +}); diff --git a/docs/user/install.md b/docs/user/install.md index 3c5718af1..00251ec0c 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -21,10 +21,10 @@ This starts the Pylon server on your machine and opens the local web app. Use ### Pylon fork -Pylon's desktop build installs beside Pylon rather than replacing it. The apps use different +Pylon's desktop build installs beside T3 Code rather than replacing it. The apps use different bundle IDs, URL handlers, Electron profiles, runtime databases, and updater metadata. On macOS the local Pylon build installs as `Pylon (Alpha).app`; its default runtime data lives under -`~/.pylon-code`, while Pylon continues using its own `.t3` and Electron data. +`~/.pylon-code`, while T3 Code continues using its own `.t3` and Electron data. From the Pylon repository, build the local macOS installer with: