Public: anyone can read this; the crew can change it.
Drop the flow_only column and the flow_only_watch table: nothing reads them since d34910d4 #8770
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/8731-drop-the-flow-only-column-and-the-flow-o → main 1 commit fork
4 files
changed +78
−1
src/core/migration-pins/20261103100000-drop-flow-only.pin added +1
| @@ -0,0 +1,1 @@ | ||
| 1 | c46ce2de352c79513ff7d8497ced7c323e849c218916920355c901ed89ae8dff |
src/core/migrations/20261103100000-drop-flow-only.ts added +9
| @@ -0,0 +1,9 @@ | ||
| 1 | // The FLOW ONLY switch left in d34910d4 (#7771), first shipped in v0.7.302; | |
| 2 | // nothing since reads `repo.flow_only` or `flow_only_watch` (#8731). Its old | |
| 3 | // `flow_only.cleared` events stay as they are, readable in a trail. | |
| 4 | // Not additive, on purpose: test/schema-additive.test.ts names it as retired | |
| 5 | // and holds the tree to never naming either again. | |
| 6 | export default ` | |
| 7 | DROP TABLE IF EXISTS flow_only_watch; | |
| 8 | ALTER TABLE repo DROP COLUMN flow_only; | |
| 9 | `; |
test/schema-additive.test.ts +38 −1
| @@ -5,7 +5,8 @@ | ||
| 5 | 5 | * read, and never adds a NOT NULL column they would not fill. |
| 6 | 6 | */ |
| 7 | 7 | import { describe, expect, test } from "bun:test"; |
| 8 | import { existsSync } from "node:fs"; | |
| 8 | import { Glob } from "bun"; | |
| 9 | import { existsSync, readFileSync } from "node:fs"; | |
| 9 | 10 | import { join } from "node:path"; |
| 10 | 11 | import releases from "../src/core/releases-before-tags.json" with { type: "json" }; |
| 11 | 12 | import { |
| @@ -70,11 +71,33 @@ describe("migrations are additive across a roll", () => { | ||
| 70 | 71 | expect(inPlay.length).toBeGreaterThan(0); |
| 71 | 72 | const offenders = migrationFiles |
| 72 | 73 | .filter((m) => inPlay.some((ids) => !ids.has(m.name))) |
| 74 | .filter((m) => !(m.name in RETIRED)) | |
| 73 | 75 | .map((m) => ({ name: m.name, problems: notAdditive(m.sql) })) |
| 74 | 76 | .filter((m) => m.problems.length > 0); |
| 75 | 77 | expect(offenders).toEqual([]); |
| 76 | 78 | }); |
| 77 | 79 | |
| 80 | test("a retired drop removes only what no file in src still names", () => { | |
| 81 | // The release that stopped reading a table or column has rolled out | |
| 82 | // (each entry says which); what keeps it true is that nothing since | |
| 83 | // names the dropped thing again, outside the migrations that made it. | |
| 84 | const named: string[] = []; | |
| 85 | for (const path of new Glob("**/*.ts").scanSync(src)) { | |
| 86 | if (path.startsWith("core/migrations/")) continue; | |
| 87 | const text = readFileSync(join(src, path), "utf8"); | |
| 88 | for (const [migration, { drops }] of Object.entries(RETIRED)) | |
| 89 | for (const name of drops) | |
| 90 | // `name.` is an event kind (`flow_only.cleared`), not the schema. | |
| 91 | if (new RegExp(`\\b${name}\\b(?!\\.)`).test(text)) named.push(`${path} names ${name} (${migration})`); | |
| 92 | } | |
| 93 | expect(named).toEqual([]); | |
| 94 | for (const [migration, { drops }] of Object.entries(RETIRED)) { | |
| 95 | const file = migrationFiles.find((m) => m.name === migration); | |
| 96 | expect(file).toBeDefined(); | |
| 97 | for (const name of drops) expect(file!.sql).toContain(name); | |
| 98 | } | |
| 99 | }); | |
| 100 | ||
| 78 | 101 | test("a release cut moves no branch: its record is a tag, not a file in the tree", () => { |
| 79 | 102 | // The releases before #6642 stay frozen beside the code; one cut since |
| 80 | 103 | // lives on its tag (release/shipped.ts). A check runs on the tree |
| @@ -117,6 +140,20 @@ describe("migrations are additive across a roll", () => { | ||
| 117 | 140 | }); |
| 118 | 141 | }); |
| 119 | 142 | |
| 143 | /** | |
| 144 | * Migrations that drop on purpose, each after the release that stopped | |
| 145 | * reading what it drops has rolled out: the migration, the names it drops, | |
| 146 | * and where the readers left. | |
| 147 | */ | |
| 148 | const RETIRED: Record<string, { readonly drops: readonly string[]; readonly since: string }> = { | |
| 149 | "20261103100000-drop-flow-only": { | |
| 150 | drops: ["flow_only_watch", "flow_only"], | |
| 151 | since: "d34910d4 (#7771), shipped from v0.7.302 (#8731)", | |
| 152 | }, | |
| 153 | }; | |
| 154 | ||
| 155 | const src = join(import.meta.dir, "../src"); | |
| 156 | ||
| 120 | 157 | interface Release { |
| 121 | 158 | readonly migrations: number; |
| 122 | 159 | readonly ids?: readonly string[]; |
test/schema-migrations.test.ts +30
| @@ -304,6 +304,36 @@ describe("statements", () => { | ||
| 304 | 304 | }); |
| 305 | 305 | }); |
| 306 | 306 | |
| 307 | describe("the retired FLOW ONLY switch leaves no schema behind (#8731)", () => { | |
| 308 | const DROP = "20261103100000-drop-flow-only"; | |
| 309 | const columns = (db: Database, table: string): string[] => | |
| 310 | db.query<{ name: string }, []>(`SELECT name FROM pragma_table_info('${table}')`).all().map((c) => c.name); | |
| 311 | ||
| 312 | test("a database that had the switch on loses the column and the table, and keeps its repositories", () => { | |
| 313 | const path = fresh(); | |
| 314 | const old = openDb(path, migrationFiles.filter((m) => m.name !== DROP).map((m) => m.sql)); | |
| 315 | expect(columns(old, "repo")).toContain("flow_only"); | |
| 316 | expect(has(old, "flow_only_watch")).toBe(true); | |
| 317 | old.exec("INSERT INTO user (id, username, email, password_hash) VALUES ('u1', 'ada', '[email protected]', 'x')"); | |
| 318 | old.exec("INSERT INTO repo (id, owner_id, name, flow_only) VALUES ('r1', 'u1', 'lab', 1)"); | |
| 319 | old.exec("INSERT INTO flow_only_watch (repo_id, alarm_at) VALUES ('r1', 1)"); | |
| 320 | old.close(); | |
| 321 | ||
| 322 | const db = openDb(path, migrations); | |
| 323 | expect(columns(db, "repo")).not.toContain("flow_only"); | |
| 324 | expect(has(db, "flow_only_watch")).toBe(false); | |
| 325 | expect(db.query("SELECT id, name FROM repo").all()).toEqual([{ id: "r1", name: "lab" }]); | |
| 326 | db.close(); | |
| 327 | }); | |
| 328 | ||
| 329 | test("a fresh database never keeps either", () => { | |
| 330 | const db = openDb(":memory:", migrations); | |
| 331 | expect(columns(db, "repo")).not.toContain("flow_only"); | |
| 332 | expect(has(db, "flow_only_watch")).toBe(false); | |
| 333 | db.close(); | |
| 334 | }); | |
| 335 | }); | |
| 336 | ||
| 307 | 337 | describe("a migration is a file named by a sortable id (#1837)", () => { |
| 308 | 338 | const source = join(import.meta.dir, "../src/core/migrations"); |
| 309 | 339 |
new above
offline