返回 DeepSeek-Reasonix
apply-diagnostics-v2.mjs
根目录 / workers / crash-report / scripts / apply-diagnostics-v2.mjs
1 #!/usr/bin/env node
2
3 import { spawnSync } from "node:child_process";
4 import { fileURLToPath } from "node:url";
5 import path from "node:path";
6
7 // metric_users and cli_metric_users were retired in #9379. Keeping them out of
8 // this contract prevents a dropped table from looking like a partial migration.
9 const columnSets = {
10 reports: ["webview2", "web_runtime"],
11 pings: [
12 "os_build", "os_revision", "channel", "distro_id", "distro_version",
13 "kernel_version", "session_type", "runtime_engine", "runtime_version", "gpu_mode",
14 ],
15 cli_pings: [
16 "os_build", "os_revision", "channel", "distro_id", "distro_version",
17 "kernel_version", "session_type", "runtime_engine", "runtime_version", "gpu_mode",
18 ],
19 };
20
21 const requiredObjects = {
22 table: ["report_daily", "report_installations", "report_event_dimensions", "diagnostics_meta"],
23 index: [
24 "report_installations_fingerprint_date",
25 "report_event_dimensions_fingerprint_date",
26 "pings_diagnostics_window",
27 ],
28 };
29
30 export const diagnosticsV2SchemaEntries = Object.freeze([
31 ...Object.entries(columnSets).flatMap(([table, columns]) =>
32 columns.map((column) => `column:${table}.${column}`)),
33 ...Object.entries(requiredObjects).flatMap(([kind, names]) =>
34 names.map((name) => `${kind}:${name}`)),
35 ]);
36
37 const tableNames = Object.keys(columnSets).map((name) => `'${name}'`).join(", ");
38 const objectNames = Object.values(requiredObjects).flat().map((name) => `'${name}'`).join(", ");
39
40 export const diagnosticsV2SchemaQuery = `
41 SELECT 'column' AS kind, m.name || '.' || p.name AS name
42 FROM sqlite_master AS m, pragma_table_info(m.name) AS p
43 WHERE m.type = 'table' AND m.name IN (${tableNames})
44 UNION ALL
45 SELECT type AS kind, name
46 FROM sqlite_master
47 WHERE type IN ('table', 'index') AND name IN (${objectNames})
48 ORDER BY kind, name;
49 `.trim();
50
51 export function parseWranglerRows(output) {
52 const payload = JSON.parse(output);
53 const rows = [];
54 let sawResults = false;
55 const visit = (value) => {
56 if (Array.isArray(value)) {
57 for (const item of value) visit(item);
58 return;
59 }
60 if (!value || typeof value !== "object") return;
61 if (Array.isArray(value.results)) {
62 sawResults = true;
63 rows.push(...value.results);
64 }
65 if (Array.isArray(value.result)) visit(value.result);
66 };
67 visit(payload);
68 if (!sawResults) throw new Error("Wrangler returned no D1 result set");
69 return rows;
70 }
71
72 export function classifyDiagnosticsV2Schema(rows) {
73 const present = new Set(rows.map((row) => `${String(row.kind)}:${String(row.name)}`));
74 const missing = diagnosticsV2SchemaEntries.filter((entry) => !present.has(entry));
75 if (missing.length === 0) return { state: "complete", missing };
76 if (missing.length === diagnosticsV2SchemaEntries.length) return { state: "absent", missing };
77 return { state: "partial", missing };
78 }
79
80 function runWrangler(projectDir, args, captureOutput = false) {
81 const executable = process.platform === "win32" ? "wrangler.cmd" : "wrangler";
82 const wrangler = path.join(projectDir, "node_modules", ".bin", executable);
83 const result = spawnSync(wrangler, args, {
84 cwd: projectDir,
85 encoding: "utf8",
86 env: process.env,
87 stdio: captureOutput ? ["ignore", "pipe", "inherit"] : "inherit",
88 });
89 if (result.error) throw result.error;
90 if (result.status !== 0) throw new Error(`wrangler exited with status ${result.status}`);
91 return result.stdout ?? "";
92 }
93
94 function inspectRemoteSchema(projectDir, database) {
95 const output = runWrangler(projectDir, [
96 "d1", "execute", database, "--remote", "--json", "--command", diagnosticsV2SchemaQuery,
97 ], true);
98 return classifyDiagnosticsV2Schema(parseWranglerRows(output));
99 }
100
101 function main() {
102 const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
103 const database = process.env.DIAGNOSTICS_D1_DATABASE || "reasonix-crash";
104 const before = inspectRemoteSchema(projectDir, database);
105 if (before.state === "complete") {
106 console.log("Diagnostics v2 D1 schema is already complete; migration skipped.");
107 return;
108 }
109 if (before.state === "partial") {
110 throw new Error(
111 `Diagnostics v2 D1 schema is partially applied; refusing the all-or-nothing migration. Missing: ${before.missing.join(", ")}`,
112 );
113 }
114
115 console.log("Recording the current D1 Time Travel bookmark before migration.");
116 runWrangler(projectDir, ["d1", "time-travel", "info", database]);
117 console.log(`Applying diagnostics v2 migration to ${database}.`);
118 runWrangler(projectDir, [
119 "d1", "execute", database, "--remote", "--yes", "--file", "migrate-diagnostics-v2.sql",
120 ]);
121 const after = inspectRemoteSchema(projectDir, database);
122 if (after.state !== "complete") {
123 throw new Error(`Diagnostics v2 D1 schema verification failed. Missing: ${after.missing.join(", ")}`);
124 }
125 console.log("Diagnostics v2 D1 schema migration and verification completed.");
126 }
127
128 const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
129 if (invokedPath === fileURLToPath(import.meta.url)) {
130 try {
131 main();
132 } catch (error) {
133 console.error(error instanceof Error ? error.message : String(error));
134 process.exitCode = 1;
135 }
136 }
137
137 lines Plain Text