Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}),
);
});
Original file line number Diff line number Diff line change
@@ -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
`;
}
});
4 changes: 2 additions & 2 deletions docs/user/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading