@xenon-device-management/xenon 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-utils.js +11 -2
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -45,14 +45,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.migrationShape = migrationShape;
|
|
49
|
-
exports.failedOnWhatWasThere = failedOnWhatWasThere;
|
|
50
48
|
exports.planSchemaSync = planSchemaSync;
|
|
51
49
|
exports.readMigrationHistory = readMigrationHistory;
|
|
52
|
-
exports.schemaMissingIn = schemaMissingIn;
|
|
53
50
|
exports.schemaSyncFailure = schemaSyncFailure;
|
|
54
51
|
exports.syncDatabaseSchema = syncDatabaseSchema;
|
|
55
|
-
exports.warnOfUnmigratedSchemaChanges = warnOfUnmigratedSchemaChanges;
|
|
56
52
|
exports.runMigrations = runMigrations;
|
|
57
53
|
const node_child_process_1 = require("node:child_process");
|
|
58
54
|
const fs = __importStar(require("node:fs"));
|
|
@@ -61,134 +57,13 @@ const os = __importStar(require("node:os"));
|
|
|
61
57
|
const logger_1 = __importDefault(require("../logger"));
|
|
62
58
|
const config_1 = require("../config");
|
|
63
59
|
const database_check_1 = require("./database-check");
|
|
64
|
-
/** A script's statements, without comments, split where a `;` ends one outside quotes. */
|
|
65
|
-
function sqlStatements(script) {
|
|
66
|
-
const statements = [];
|
|
67
|
-
let current = '';
|
|
68
|
-
let i = 0;
|
|
69
|
-
while (i < script.length) {
|
|
70
|
-
const c = script[i];
|
|
71
|
-
if (c === '-' && script[i + 1] === '-') {
|
|
72
|
-
const end = script.indexOf('\n', i);
|
|
73
|
-
i = end === -1 ? script.length : end;
|
|
74
|
-
current += ' ';
|
|
75
|
-
}
|
|
76
|
-
else if (c === '/' && script[i + 1] === '*') {
|
|
77
|
-
const end = script.indexOf('*/', i + 2);
|
|
78
|
-
i = end === -1 ? script.length : end + 2;
|
|
79
|
-
current += ' ';
|
|
80
|
-
}
|
|
81
|
-
else if (c === "'" || c === '"' || c === '`' || c === '[') {
|
|
82
|
-
const close = c === '[' ? ']' : c;
|
|
83
|
-
let j = i + 1;
|
|
84
|
-
while (j < script.length) {
|
|
85
|
-
if (script[j] === close && close !== ']' && script[j + 1] === close)
|
|
86
|
-
j += 2;
|
|
87
|
-
else if (script[j] === close)
|
|
88
|
-
break;
|
|
89
|
-
else
|
|
90
|
-
j++;
|
|
91
|
-
}
|
|
92
|
-
current += script.slice(i, j + 1);
|
|
93
|
-
i = j + 1;
|
|
94
|
-
}
|
|
95
|
-
else if (c === ';') {
|
|
96
|
-
if (current.trim())
|
|
97
|
-
statements.push(current.trim());
|
|
98
|
-
current = '';
|
|
99
|
-
i++;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
current += c;
|
|
103
|
-
i++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (current.trim())
|
|
107
|
-
statements.push(current.trim());
|
|
108
|
-
return statements;
|
|
109
|
-
}
|
|
110
|
-
/** The part of a statement a table comparison sees: a table, a column, an index. */
|
|
111
|
-
const SEEN_DDL = /^(CREATE (UNIQUE )?INDEX|CREATE TABLE|ALTER TABLE|DROP TABLE|DROP INDEX)\b/;
|
|
112
|
-
/** Prisma's copy of a table it redefines: every row, into the table that replaces it. */
|
|
113
|
-
const REDEFINE_COPY = /^INSERT INTO "new_([^"]+)" \([^)]*\) SELECT .* FROM "\1"$/;
|
|
114
|
-
/** What a migration does that the tables can and can't show. */
|
|
115
|
-
function migrationShape(script) {
|
|
116
|
-
let statements = 0;
|
|
117
|
-
const unseen = [];
|
|
118
|
-
for (const statement of sqlStatements(script)) {
|
|
119
|
-
const flat = statement.replace(/\s+/g, ' ');
|
|
120
|
-
const upper = flat.toUpperCase();
|
|
121
|
-
if (upper.startsWith('PRAGMA '))
|
|
122
|
-
continue;
|
|
123
|
-
statements++;
|
|
124
|
-
const seen = (SEEN_DDL.test(upper) &&
|
|
125
|
-
!/\bCOLLATE\b/.test(upper) &&
|
|
126
|
-
!/^CREATE TABLE .* AS SELECT\b/.test(upper)) ||
|
|
127
|
-
REDEFINE_COPY.test(flat);
|
|
128
|
-
if (!seen)
|
|
129
|
-
unseen.push(flat.length > 60 ? `${flat.slice(0, 57)}...` : flat);
|
|
130
|
-
}
|
|
131
|
-
return { statements, unseen };
|
|
132
|
-
}
|
|
133
|
-
/** Whether a migration that failed can be run again without doing any of it twice. */
|
|
134
|
-
function rerunnable(shape) {
|
|
135
|
-
// A failed statement changed nothing (SQLite undoes it), so one statement
|
|
136
|
-
// did nothing. Several whose every effect shows: a part that ran is still
|
|
137
|
-
// there, and running it again fails on it rather than doing it twice.
|
|
138
|
-
return shape.statements <= 1 || shape.unseen.length === 0;
|
|
139
|
-
}
|
|
140
|
-
/** Whether a failed migration's error says what it makes was already there. */
|
|
141
|
-
function failedOnWhatWasThere(logs) {
|
|
142
|
-
return /already exists|duplicate column name/i.test(logs !== null && logs !== void 0 ? logs : '');
|
|
143
|
-
}
|
|
144
|
-
/** For a planner given no SQL: every migration changes only what the tables show. */
|
|
145
|
-
const SHOWN = () => ({ statements: 1, unseen: [] });
|
|
146
60
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* It never goes below the last migration the history records, and gives up
|
|
152
|
-
* (null) if a comparison can't be made. Every migration comes first, as `db
|
|
153
|
-
* push` leaves the tables, then the history's own, then the rest from the top.
|
|
61
|
+
* Which command brings a database up to date. `historyMatchesTables` is asked
|
|
62
|
+
* only about a database with migrations left to apply. Given the applied
|
|
63
|
+
* migrations, it says whether the tables are what they make, or null when it
|
|
64
|
+
* can't tell.
|
|
154
65
|
*/
|
|
155
|
-
function
|
|
156
|
-
const applied = new Set(history.applied);
|
|
157
|
-
const lowest = localMigrations.reduce((last, name, i) => (applied.has(name) ? i + 1 : last), 0);
|
|
158
|
-
const ask = (k) => {
|
|
159
|
-
if (!known.has(k))
|
|
160
|
-
known.set(k, tablesMatch(localMigrations.slice(0, k)));
|
|
161
|
-
return known.get(k);
|
|
162
|
-
};
|
|
163
|
-
const order = [localMigrations.length, lowest];
|
|
164
|
-
for (let k = localMigrations.length - 1; k > lowest; k--)
|
|
165
|
-
order.push(k);
|
|
166
|
-
for (let k of new Set(order)) {
|
|
167
|
-
const matches = ask(k);
|
|
168
|
-
if (matches === null)
|
|
169
|
-
return null;
|
|
170
|
-
if (matches === false)
|
|
171
|
-
continue;
|
|
172
|
-
while (k > lowest) {
|
|
173
|
-
const below = ask(k - 1);
|
|
174
|
-
if (below === null)
|
|
175
|
-
return null;
|
|
176
|
-
if (below === false)
|
|
177
|
-
break;
|
|
178
|
-
k--;
|
|
179
|
-
}
|
|
180
|
-
return k;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Which command brings a database up to date, and what to record first.
|
|
186
|
-
* `tablesMatch` is asked only about a database with migrations left to apply
|
|
187
|
-
* or a failed one recorded. Given a run of migrations, it says whether the
|
|
188
|
-
* tables are what they make, or null when it can't tell. `shapeOf` says what
|
|
189
|
-
* a migration does that the tables can't show.
|
|
190
|
-
*/
|
|
191
|
-
function planSchemaSync(history, localMigrations, tablesMatch, shapeOf = SHOWN) {
|
|
66
|
+
function planSchemaSync(history, localMigrations, historyMatchesTables) {
|
|
192
67
|
if (!history) {
|
|
193
68
|
return {
|
|
194
69
|
command: 'db push',
|
|
@@ -198,138 +73,44 @@ function planSchemaSync(history, localMigrations, tablesMatch, shapeOf = SHOWN)
|
|
|
198
73
|
failed: [],
|
|
199
74
|
};
|
|
200
75
|
}
|
|
201
|
-
|
|
202
|
-
const recorded = localMigrations.filter((name) => applied.has(name));
|
|
203
|
-
const known = new Map();
|
|
204
|
-
if (history.failed.length === 0) {
|
|
205
|
-
const pending = localMigrations.filter((name) => !applied.has(name));
|
|
206
|
-
if (pending.length === 0) {
|
|
207
|
-
return {
|
|
208
|
-
command: 'migrate deploy',
|
|
209
|
-
acceptDataLoss: false,
|
|
210
|
-
reason: 'its migration history has every migration',
|
|
211
|
-
basis: 'complete',
|
|
212
|
-
failed: [],
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const matches = tablesMatch(recorded);
|
|
216
|
-
if (matches !== false) {
|
|
217
|
-
return {
|
|
218
|
-
command: 'migrate deploy',
|
|
219
|
-
acceptDataLoss: false,
|
|
220
|
-
reason: `its migration history is missing ${pending.length} migration(s)` +
|
|
221
|
-
(matches === null ? ' (the tables could not be compared with the history)' : ''),
|
|
222
|
-
basis: matches === null ? 'uncompared' : 'behind',
|
|
223
|
-
failed: [],
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
// The history's own run, already compared, when it is the first n.
|
|
227
|
-
if (recorded.every((name, i) => localMigrations[i] === name)) {
|
|
228
|
-
known.set(recorded.length, false);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// A history from a later release, or another branch's: not this release's to rewrite or run.
|
|
232
|
-
const local = new Set(localMigrations);
|
|
233
|
-
const unknown = [...history.applied, ...history.failed].filter((name) => !local.has(name));
|
|
234
|
-
if (unknown.length > 0) {
|
|
76
|
+
if (history.failed.length > 0) {
|
|
235
77
|
return {
|
|
236
78
|
command: 'db push',
|
|
237
79
|
acceptDataLoss: false,
|
|
238
|
-
reason: `its migration history
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
basis: 'other-release',
|
|
80
|
+
reason: `its migration history records a failed migration (${history.failed.join(', ')}), ` +
|
|
81
|
+
'which migrate deploy will not get past',
|
|
82
|
+
basis: 'failed-migration',
|
|
242
83
|
failed: history.failed,
|
|
243
|
-
unknown,
|
|
244
84
|
};
|
|
245
85
|
}
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
command: 'none',
|
|
250
|
-
acceptDataLoss: false,
|
|
251
|
-
reason: `its migration history records ${migration} as failed, and ${detail}: Xenon neither ` +
|
|
252
|
-
'runs it again nor records it as applied',
|
|
253
|
-
basis: 'failed-unresolved',
|
|
254
|
-
failed,
|
|
255
|
-
unresolved: { migration, why, logs: logsOf(migration) },
|
|
256
|
-
});
|
|
257
|
-
const k = rebaseline(history, localMigrations, tablesMatch, known);
|
|
258
|
-
if (k !== null) {
|
|
259
|
-
const within = new Set(localMigrations.slice(0, k));
|
|
260
|
-
const toRecord = localMigrations.slice(0, k).filter((name) => !applied.has(name));
|
|
261
|
-
for (const name of failed) {
|
|
262
|
-
const shape = shapeOf(name);
|
|
263
|
-
if (within.has(name)) {
|
|
264
|
-
// The tables have what it makes: recorded as applied only if it failed
|
|
265
|
-
// because that was there already, and it does nothing they can't show.
|
|
266
|
-
if (!failedOnWhatWasThere(logsOf(name))) {
|
|
267
|
-
return unresolved(name, 'part-way', 'the tables have what it makes, though it failed on something other than that ' +
|
|
268
|
-
'being there already, so part of it may have run before it failed');
|
|
269
|
-
}
|
|
270
|
-
if (shape.unseen.length > 0) {
|
|
271
|
-
return unresolved(name, 'unseen-part', 'what it makes was in the tables already when it ran, but it also changes what ' +
|
|
272
|
-
`the tables can't show (${shape.unseen[0]}), which never ran`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
else if (!rerunnable(shape)) {
|
|
276
|
-
return unresolved(name, 'part-way', `its ${shape.statements} statements change what the tables can't show ` +
|
|
277
|
-
`(${shape.unseen[0]}), so the tables can't tell how many of them ran before it failed`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const unseen = toRecord.filter((name) => !failed.includes(name) && shapeOf(name).unseen.length > 0);
|
|
281
|
-
if (unseen.length > 0) {
|
|
282
|
-
return {
|
|
283
|
-
command: 'db push',
|
|
284
|
-
acceptDataLoss: false,
|
|
285
|
-
reason: `its tables are what its first ${k} migrations make, though its history records ` +
|
|
286
|
-
`${recorded.length}, but ${unseen.join(', ')} ${unseen.length > 1 ? 'change' : 'changes'} ` +
|
|
287
|
-
`what the tables can't show (${shapeOf(unseen[0]).unseen[0]}), so Xenon can't tell ` +
|
|
288
|
-
'whether that part ran, leaves the history as it is and uses db push, as before',
|
|
289
|
-
basis: 'unrecordable',
|
|
290
|
-
failed,
|
|
291
|
-
unseen,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
const rolledBack = failed;
|
|
86
|
+
const applied = new Set(history.applied);
|
|
87
|
+
const pending = localMigrations.filter((name) => !applied.has(name));
|
|
88
|
+
if (pending.length === 0) {
|
|
295
89
|
return {
|
|
296
90
|
command: 'migrate deploy',
|
|
297
91
|
acceptDataLoss: false,
|
|
298
|
-
reason:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
': Xenon ' +
|
|
302
|
-
(rolledBack.length ? `marks ${rolledBack.join(', ')} rolled back, ` : '') +
|
|
303
|
-
(toRecord.length ? `records ${toRecord.join(', ')} as applied, ` : '') +
|
|
304
|
-
'then runs migrate deploy',
|
|
305
|
-
basis: 'rebaselined',
|
|
306
|
-
failed,
|
|
307
|
-
resolve: { rolledBack, applied: toRecord },
|
|
92
|
+
reason: 'its migration history has every migration',
|
|
93
|
+
basis: 'complete',
|
|
94
|
+
failed: [],
|
|
308
95
|
};
|
|
309
96
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const unsafe = failed.find((name) => !rerunnable(shapeOf(name)));
|
|
313
|
-
if (unsafe) {
|
|
314
|
-
const shape = shapeOf(unsafe);
|
|
315
|
-
return unresolved(unsafe, 'part-way', `its ${shape.statements} statements change what the tables can't show ` +
|
|
316
|
-
`(${shape.unseen[0]}), so the tables can't tell how many of them ran before it failed`);
|
|
317
|
-
}
|
|
97
|
+
const matches = historyMatchesTables(localMigrations.filter((name) => applied.has(name)));
|
|
98
|
+
if (matches === false) {
|
|
318
99
|
return {
|
|
319
100
|
command: 'db push',
|
|
320
101
|
acceptDataLoss: false,
|
|
321
|
-
reason:
|
|
322
|
-
'
|
|
323
|
-
basis: '
|
|
324
|
-
failed,
|
|
102
|
+
reason: 'its tables differ from what its recorded migrations make (db push updated it, or it ' +
|
|
103
|
+
'was changed by hand), so migrate deploy could fail on a change that is already there',
|
|
104
|
+
basis: 'tables-differ',
|
|
105
|
+
failed: [],
|
|
325
106
|
};
|
|
326
107
|
}
|
|
327
108
|
return {
|
|
328
|
-
command: '
|
|
109
|
+
command: 'migrate deploy',
|
|
329
110
|
acceptDataLoss: false,
|
|
330
|
-
reason:
|
|
331
|
-
|
|
332
|
-
basis: '
|
|
111
|
+
reason: `its migration history is missing ${pending.length} migration(s)` +
|
|
112
|
+
(matches === null ? ' (the tables could not be compared with the history)' : ''),
|
|
113
|
+
basis: matches === null ? 'uncompared' : 'behind',
|
|
333
114
|
failed: [],
|
|
334
115
|
};
|
|
335
116
|
}
|
|
@@ -346,19 +127,6 @@ function sqliteFilePath(databaseUrl, baseDir) {
|
|
|
346
127
|
return file;
|
|
347
128
|
return baseDir ? path.resolve(baseDir, file) : null;
|
|
348
129
|
}
|
|
349
|
-
/** What Prisma wrote down when each failed migration failed; nothing when it can't be read. */
|
|
350
|
-
function failureLogsIn(client) {
|
|
351
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
-
try {
|
|
353
|
-
const rows = yield client.$queryRawUnsafe('SELECT migration_name, logs FROM "_prisma_migrations" ' +
|
|
354
|
-
'WHERE finished_at IS NULL AND rolled_back_at IS NULL');
|
|
355
|
-
return Object.fromEntries(rows.map((r) => { var _a; return [r.migration_name, String((_a = r.logs) !== null && _a !== void 0 ? _a : '')]; }));
|
|
356
|
-
}
|
|
357
|
-
catch (_a) {
|
|
358
|
-
return {};
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
130
|
/** The migration history in `_prisma_migrations`, or null when the database has none. */
|
|
363
131
|
function readMigrationHistory(databaseUrl) {
|
|
364
132
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -370,10 +138,10 @@ function readMigrationHistory(databaseUrl) {
|
|
|
370
138
|
const client = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
|
|
371
139
|
try {
|
|
372
140
|
const rows = yield client.$queryRawUnsafe('SELECT migration_name, finished_at, rolled_back_at FROM "_prisma_migrations"');
|
|
373
|
-
|
|
374
|
-
.filter((r) =>
|
|
375
|
-
.map((r) => r.migration_name)
|
|
376
|
-
|
|
141
|
+
return {
|
|
142
|
+
applied: rows.filter((r) => r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
|
|
143
|
+
failed: rows.filter((r) => !r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
|
|
144
|
+
};
|
|
377
145
|
}
|
|
378
146
|
catch (err) {
|
|
379
147
|
const message = String((_c = (_b = (_a = err === null || err === void 0 ? void 0 : err.meta) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err);
|
|
@@ -393,32 +161,6 @@ function readMigrationHistory(databaseUrl) {
|
|
|
393
161
|
}
|
|
394
162
|
});
|
|
395
163
|
}
|
|
396
|
-
/**
|
|
397
|
-
* A consistent copy of a SQLite file, made by SQLite itself (`VACUUM INTO`)
|
|
398
|
-
* through a short-lived Prisma client, as the history is read. A copy of the
|
|
399
|
-
* file's bytes alone would be wrong beside a hot journal or a WAL.
|
|
400
|
-
*/
|
|
401
|
-
function copyWithVacuum(from, to) {
|
|
402
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
403
|
-
const { PrismaClient } = yield Promise.resolve().then(() => __importStar(require('../generated/client')));
|
|
404
|
-
const client = new PrismaClient({ datasources: { db: { url: `file:${from}` } } });
|
|
405
|
-
try {
|
|
406
|
-
yield client.$executeRawUnsafe(`VACUUM INTO '${to.replace(/'/g, "''")}'`);
|
|
407
|
-
}
|
|
408
|
-
finally {
|
|
409
|
-
yield client.$disconnect();
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
function freeBytesIn(dir) {
|
|
414
|
-
try {
|
|
415
|
-
const stats = fs.statfsSync(dir);
|
|
416
|
-
return Number(stats.bavail) * Number(stats.bsize);
|
|
417
|
-
}
|
|
418
|
-
catch (_a) {
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
164
|
/** The migrations in prisma/migrations, in the order Prisma applies them. */
|
|
423
165
|
function localMigrationsIn(migrationsDir) {
|
|
424
166
|
return fs
|
|
@@ -433,275 +175,96 @@ function shellWord(word) {
|
|
|
433
175
|
/**
|
|
434
176
|
* The commands a failure's message gives, ready to paste. A copy is made with
|
|
435
177
|
* SQLite's `VACUUM INTO` through `prisma db execute`, so it is consistent
|
|
436
|
-
* whatever journal sits beside the file, and needs nothing but the CLI.
|
|
437
|
-
* goes into an empty file given the database's own mode first: `VACUUM INTO`
|
|
438
|
-
* takes an empty file, and refuses one that isn't, so a backup never
|
|
439
|
-
* overwrites another.
|
|
178
|
+
* whatever journal sits beside the file, and needs nothing but the CLI.
|
|
440
179
|
*/
|
|
441
180
|
function pasteable(at) {
|
|
442
181
|
const schema = shellWord(at.schema);
|
|
443
|
-
const url =
|
|
182
|
+
const url = (file) => `file:${file}`;
|
|
444
183
|
return {
|
|
445
|
-
|
|
184
|
+
copyTo(target) {
|
|
446
185
|
const statement = `VACUUM INTO '${target.replace(/'/g, "''")}'`;
|
|
447
186
|
const quoted = /["$`\\!]/.test(statement) ? shellWord(statement) : `"${statement}"`;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const mode = (at.mode & 0o777).toString(8).padStart(3, '0');
|
|
453
|
-
return `touch ${word} && chmod ${mode} ${word} && ${copy}`;
|
|
187
|
+
return `echo ${quoted} | ${at.prisma} db execute --url ${shellWord(at.databaseUrl)} --stdin`;
|
|
188
|
+
},
|
|
189
|
+
deploy(file) {
|
|
190
|
+
return `DATABASE_URL=${shellWord(url(file))} ${at.prisma} migrate deploy --schema ${schema}`;
|
|
454
191
|
},
|
|
455
192
|
letGo() {
|
|
456
|
-
return (`DATABASE_URL=${
|
|
193
|
+
return (`DATABASE_URL=${shellWord(at.databaseUrl)} ${at.prisma} db push --skip-generate ` +
|
|
457
194
|
`--accept-data-loss --schema ${schema}`);
|
|
458
195
|
},
|
|
459
|
-
resolve(flag, migration) {
|
|
460
|
-
return (`DATABASE_URL=${url} ${at.prisma} migrate resolve ${flag} ${shellWord(migration)} ` +
|
|
461
|
-
`--schema ${schema}`);
|
|
462
|
-
},
|
|
463
196
|
};
|
|
464
197
|
}
|
|
465
|
-
/** A backup's name: the file's, stamped with the time
|
|
198
|
+
/** A backup's name: the file's, stamped with the time, so one never overwrites another. */
|
|
466
199
|
function backupOf(file, now) {
|
|
467
|
-
|
|
200
|
+
const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
|
|
201
|
+
return `${file}.backup-${stamp}`;
|
|
468
202
|
}
|
|
469
203
|
/** The migration a P3018 names, if it does. */
|
|
470
204
|
function failedMigrationIn(output) {
|
|
471
205
|
const match = /Migration name:\s*(\S+)/.exec(output);
|
|
472
206
|
return match ? match[1] : null;
|
|
473
207
|
}
|
|
474
|
-
const megabytes = (bytes) => Math.ceil(bytes / (1024 * 1024));
|
|
475
|
-
/**
|
|
476
|
-
* What a database lacks of what should be there (prisma/schema.prisma, or
|
|
477
|
-
* what a run of migrations makes), from Prisma's summary of a diff from that
|
|
478
|
-
* to the database: every line that removes or changes something (with its
|
|
479
|
-
* table's line), or null when it only adds (what was added to the database
|
|
480
|
-
* by hand) or finds nothing. A line it doesn't know counts as lacking.
|
|
481
|
-
*/
|
|
482
|
-
function schemaMissingIn(summary) {
|
|
483
|
-
const missing = [];
|
|
484
|
-
let header = '';
|
|
485
|
-
for (const line of summary.split('\n').map((l) => l.trimEnd())) {
|
|
486
|
-
if (!line.trim() || /^No difference detected\.?$/.test(line.trim()))
|
|
487
|
-
continue;
|
|
488
|
-
if (!/^\s/.test(line)) {
|
|
489
|
-
// A table's heading: added tables, or a table whose lines say what changed.
|
|
490
|
-
header = line;
|
|
491
|
-
if (!/^\[\+\]/.test(line) && !/^\[\*\] Changed the `[^`]+` table$/.test(line)) {
|
|
492
|
-
missing.push(line);
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const item = line.trim();
|
|
497
|
-
const adds = /^\[\+\]/.test(item) || (/^- /.test(item) && /^\[\+\]/.test(header));
|
|
498
|
-
if (adds)
|
|
499
|
-
continue;
|
|
500
|
-
if (!missing.includes(header))
|
|
501
|
-
missing.push(header);
|
|
502
|
-
missing.push(line);
|
|
503
|
-
}
|
|
504
|
-
return missing.length ? missing.join('\n') : null;
|
|
505
|
-
}
|
|
506
|
-
/** What the copy told about the database, for a refusal's first sentence and its way to keep. */
|
|
507
|
-
function copyTrialText(trial, basis) {
|
|
508
|
-
const thenTries = "and start again: Xenon then tries this version's migrations on a copy first, and uses " +
|
|
509
|
-
'them if they work there.';
|
|
510
|
-
if (!trial || trial.outcome === 'worked')
|
|
511
|
-
return { why: '', keep: null };
|
|
512
|
-
if (trial.outcome === 'failed') {
|
|
513
|
-
return {
|
|
514
|
-
why: ", and this version's migrations failed on a copy of it" +
|
|
515
|
-
(trial.migration ? ` (${trial.migration})` : ''),
|
|
516
|
-
keep: null,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
if (trial.outcome === 'incomplete' && trial.stage === 'before') {
|
|
520
|
-
return {
|
|
521
|
-
why: (basis === 'tables-differ'
|
|
522
|
-
? ': they lack part of what those make'
|
|
523
|
-
: ', and its tables lack part of what its recorded migrations make') +
|
|
524
|
-
" (listed at the end), so Xenon does not try this version's migrations on a copy of it",
|
|
525
|
-
keep: `To keep it, add what the tables lack to the database ${thenTries}`,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
if (trial.outcome === 'incomplete') {
|
|
529
|
-
return {
|
|
530
|
-
why: ", and after this version's migrations a copy of it still lacked part of this " +
|
|
531
|
-
"version's schema (listed at the end), so Xenon doesn't run them on it",
|
|
532
|
-
keep: 'To keep it, add what the copy lacked to the database (prisma/schema.prisma says what ' +
|
|
533
|
-
`it is) ${thenTries}`,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
if (trial.why === 'space') {
|
|
537
|
-
const free = trial.neededBytes
|
|
538
|
-
? `free ${megabytes(trial.neededBytes)} MB next to the file`
|
|
539
|
-
: 'make room next to the file';
|
|
540
|
-
return {
|
|
541
|
-
why: `, and there was no room next to it for a copy to try this version's migrations on (${trial.detail})`,
|
|
542
|
-
keep: `To keep it, ${free} ${thenTries}`,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
if (trial.why === 'locked') {
|
|
546
|
-
return {
|
|
547
|
-
why: `, and it was locked, so Xenon could not copy it to try this version's migrations on (${trial.detail})`,
|
|
548
|
-
keep: `To keep it, stop whatever else is using the file (another server?) ${thenTries}`,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
return {
|
|
552
|
-
why: `, and Xenon could not copy it to try this version's migrations on (${trial.detail})`,
|
|
553
|
-
keep: `To keep it, fix what stopped the copy ${thenTries}`,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
/** Why `db push` was the command, for its refusal's first sentence. */
|
|
557
|
-
function dbPushBecause(plan) {
|
|
558
|
-
var _a, _b;
|
|
559
|
-
switch (plan.basis) {
|
|
560
|
-
case 'failed-migration':
|
|
561
|
-
return {
|
|
562
|
-
why: `its migration history records a failed migration (${plan.failed.join(', ')}), ` +
|
|
563
|
-
"which prisma migrate deploy won't run past",
|
|
564
|
-
keep: null,
|
|
565
|
-
};
|
|
566
|
-
case 'other-release':
|
|
567
|
-
return {
|
|
568
|
-
why: 'its migration history names migrations this version of Xenon does not have ' +
|
|
569
|
-
`(${((_a = plan.unknown) !== null && _a !== void 0 ? _a : []).join(', ')}): another release brought it up to date`,
|
|
570
|
-
keep: 'To keep it, start it with the release of Xenon that last brought it up to date.',
|
|
571
|
-
};
|
|
572
|
-
case 'unrecordable':
|
|
573
|
-
return {
|
|
574
|
-
why: 'its tables are ahead of its migration history, and the migrations they are ahead ' +
|
|
575
|
-
`by include ${((_b = plan.unseen) !== null && _b !== void 0 ? _b : []).join(', ')}, which change(s) what the tables ` +
|
|
576
|
-
"can't show, so Xenon can't record them as applied",
|
|
577
|
-
keep: null,
|
|
578
|
-
};
|
|
579
|
-
default:
|
|
580
|
-
return {
|
|
581
|
-
why: 'its tables differ from what its recorded migrations make (something was added to ' +
|
|
582
|
-
'them by hand, say)',
|
|
583
|
-
keep: null,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
/** What Prisma wrote down when a migration failed, without the engine's stack trace. */
|
|
588
|
-
function withoutStack(logs) {
|
|
589
|
-
return logs.split(/\n\s+0: /)[0].trim();
|
|
590
|
-
}
|
|
591
|
-
/** What to do about a failed migration Xenon neither runs again nor records as applied. */
|
|
592
|
-
function unresolvedAdvice(migration, why, where, at, now) {
|
|
593
|
-
const run = pasteable(at);
|
|
594
|
-
const sql = at.migrationsDir
|
|
595
|
-
? path.join(at.migrationsDir, migration, 'migration.sql')
|
|
596
|
-
: `prisma/migrations/${migration}/migration.sql`;
|
|
597
|
-
const reason = why === 'unseen-part'
|
|
598
|
-
? 'What it makes was already in the tables when it ran, but it also changes rows (or ' +
|
|
599
|
-
"makes a trigger, a view or a collation), which the tables can't show, and that " +
|
|
600
|
-
'part never ran'
|
|
601
|
-
: "SQLite keeps what a migration's statements did up to the one that failed, so part " +
|
|
602
|
-
'of it may be in the tables';
|
|
603
|
-
return (`A migration, ${migration}, failed on the database at ${where}, and its migration ` +
|
|
604
|
-
`history records it as failed (Prisma's reason is below). ${reason}: Xenon neither runs ` +
|
|
605
|
-
'it again nor records it as applied. ' +
|
|
606
|
-
(at.file
|
|
607
|
-
? `Back the file up:\n ${run.backup(backupOf(at.file, now))}\n`
|
|
608
|
-
: 'Back the database up. ') +
|
|
609
|
-
`Then compare what it does (${sql}) with the tables and, once the rows it failed on are ` +
|
|
610
|
-
"changed, either finish what it didn't do and record it as applied:\n" +
|
|
611
|
-
` ${run.resolve('--applied', migration)}\n` +
|
|
612
|
-
'or undo what it did and record it as rolled back, so that the next start runs it again:\n' +
|
|
613
|
-
` ${run.resolve('--rolled-back', migration)}\n` +
|
|
614
|
-
'Then start again.');
|
|
615
|
-
}
|
|
616
208
|
/**
|
|
617
209
|
* What went wrong and what to do, then Prisma's own output. A refusal to
|
|
618
210
|
* delete data gives the commands to run, with this server's paths, and never
|
|
619
|
-
* sends `migrate deploy` to a database
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* (P3018, which records the migration as failed). `step` names a failed call
|
|
623
|
-
* other than the plan's command; `failed` describes the migration a P3018
|
|
624
|
-
* names.
|
|
211
|
+
* sends `migrate deploy` to a database it would fail on: one with a failed
|
|
212
|
+
* migration recorded (P3009), or one whose tables are ahead of its history
|
|
213
|
+
* (P3018, which records the migration as failed).
|
|
625
214
|
*/
|
|
626
|
-
function schemaSyncFailure(plan, output, at,
|
|
627
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
215
|
+
function schemaSyncFailure(plan, output, at, now = new Date()) {
|
|
628
216
|
const where = (0, database_check_1.describeDatabaseUrl)(at.databaseUrl);
|
|
629
217
|
const run = pasteable(at);
|
|
630
218
|
let sentence;
|
|
631
|
-
|
|
632
|
-
if (plan.basis === 'failed-unresolved' && plan.unresolved) {
|
|
633
|
-
sentence = unresolvedAdvice(plan.unresolved.migration, plan.unresolved.why, where, at, now);
|
|
634
|
-
}
|
|
635
|
-
else if (step) {
|
|
636
|
-
sentence =
|
|
637
|
-
`prisma ${step} could not correct the migration history of the database at ${where}: ` +
|
|
638
|
-
'back the file up and fix what Prisma reports below, then start again.';
|
|
639
|
-
}
|
|
640
|
-
else if (/Added the required column/i.test(output)) {
|
|
219
|
+
if (/Added the required column/i.test(output)) {
|
|
641
220
|
sentence =
|
|
642
221
|
`This version adds a required column to a table that already has rows in ${where}, ` +
|
|
643
222
|
'which prisma db push cannot do: back the file up, then migrate it by hand or start ' +
|
|
644
223
|
'from a fresh database.';
|
|
645
224
|
}
|
|
646
225
|
else if (/accept-data-loss|There might be data loss/i.test(output)) {
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
226
|
+
const why = plan.basis === 'failed-migration'
|
|
227
|
+
? `its migration history records a failed migration (${plan.failed.join(', ')}), ` +
|
|
228
|
+
"which prisma migrate deploy won't run past"
|
|
229
|
+
: 'its tables differ from what its recorded migrations make (something was added to ' +
|
|
230
|
+
'them by hand, or prisma db push moved them past the history)';
|
|
231
|
+
const backup = at.file
|
|
232
|
+
? `Back the file up first:\n ${run.copyTo(backupOf(at.file, now))}\n`
|
|
233
|
+
: 'Back the database up first. ';
|
|
655
234
|
sentence =
|
|
656
235
|
'prisma db push stopped rather than delete what Prisma lists below from the database ' +
|
|
657
|
-
`at ${where}. Xenon brings this database up to date with prisma db push because ` +
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
` && ${run.letGo()}\n`
|
|
664
|
-
: 'To let what is listed go, back the database up, then run this and start again:\n' +
|
|
665
|
-
` ${run.letGo()}\n`) +
|
|
666
|
-
((_c = (_b = trial.keep) !== null && _b !== void 0 ? _b : because.keep) !== null && _c !== void 0 ? _c : 'To keep any of it, copy it out of the database first.');
|
|
667
|
-
if (((_d = plan.trial) === null || _d === void 0 ? void 0 : _d.outcome) === 'failed') {
|
|
668
|
-
after = `\n\nOn the copy, prisma migrate deploy said:\n${plan.trial.output}`;
|
|
236
|
+
`at ${where}. Xenon brings this database up to date with prisma db push because ${why}, ` +
|
|
237
|
+
'and without --accept-data-loss db push deletes no table or column that holds data. ' +
|
|
238
|
+
backup +
|
|
239
|
+
`To let what is listed go, run this and start again:\n ${run.letGo()}\n`;
|
|
240
|
+
if (plan.basis === 'failed-migration' || !at.file) {
|
|
241
|
+
sentence += 'To keep any of it, copy it out of the database first.';
|
|
669
242
|
}
|
|
670
|
-
else
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
243
|
+
else {
|
|
244
|
+
// migrate deploy works on the file only if the tables are not ahead of
|
|
245
|
+
// its history, which nothing here can tell: a copy finds out.
|
|
246
|
+
const copy = `${at.file}.copy`;
|
|
247
|
+
sentence +=
|
|
248
|
+
"To keep it, try this version's migrations on a copy of the file instead:\n" +
|
|
249
|
+
` ${run.copyTo(copy)}\n` +
|
|
250
|
+
` ${run.deploy(copy)}\n` +
|
|
251
|
+
'If that works, run them on the file, delete the copy and start again:\n' +
|
|
252
|
+
` ${run.deploy(at.file)}\n` +
|
|
253
|
+
'If it fails, they would fail on the file too (P3018 means the tables already have ' +
|
|
254
|
+
"a change the history doesn't record): delete the copy, copy out anything you need, " +
|
|
255
|
+
'and let what is listed go as above.';
|
|
679
256
|
}
|
|
680
257
|
}
|
|
681
258
|
else if (/P3018/.test(output)) {
|
|
682
|
-
const name =
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
sentence =
|
|
692
|
-
`${failedOn}, and its migration history now records it as failed. Check Prisma's ` +
|
|
693
|
-
'reason below. If it failed on the rows in the database (a unique index over ' +
|
|
694
|
-
'duplicate values, say), back the file up, change those rows, and start again: the ' +
|
|
695
|
-
'next start runs it again.';
|
|
696
|
-
}
|
|
697
|
-
else if (failed) {
|
|
698
|
-
sentence = unresolvedAdvice(failed.migration, failed.wasThere ? 'unseen-part' : 'part-way', where, at, now);
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
sentence =
|
|
702
|
-
`${failedOn}, and its migration history now records it as failed. Check Prisma's ` +
|
|
703
|
-
'reason below, back the file up and fix it, then start again.';
|
|
704
|
-
}
|
|
259
|
+
const name = failedMigrationIn(output);
|
|
260
|
+
sentence =
|
|
261
|
+
`A migration${name ? `, ${name},` : ''} failed on the database at ${where}, and its ` +
|
|
262
|
+
"migration history now records it as failed. Check Prisma's reason below. If what the " +
|
|
263
|
+
'migration makes is already there ("already exists", "duplicate column name"), start ' +
|
|
264
|
+
'again: Xenon brings a database with a failed migration recorded up to date with ' +
|
|
265
|
+
'prisma db push, which stops before deleting a table or column that holds data. If ' +
|
|
266
|
+
'the migration failed on the rows in the database (a unique index over duplicate ' +
|
|
267
|
+
'values, say), back the file up, change those rows, and start again.';
|
|
705
268
|
}
|
|
706
269
|
else {
|
|
707
270
|
sentence =
|
|
@@ -709,7 +272,7 @@ function schemaSyncFailure(plan, output, at, { now = new Date(), step, failed, }
|
|
|
709
272
|
'file up and fix what Prisma reports below, or, if its schema already matches this ' +
|
|
710
273
|
'version, set XENON_AUTO_MIGRATE=false to start without changing it.';
|
|
711
274
|
}
|
|
712
|
-
return `[DBMigrate] Cannot start: ${sentence}\n\n${output}
|
|
275
|
+
return `[DBMigrate] Cannot start: ${sentence}\n\n${output}`;
|
|
713
276
|
}
|
|
714
277
|
/** What a failed prisma call printed: its warnings (stdout) and its error (stderr). */
|
|
715
278
|
function cliOutput(err) {
|
|
@@ -746,174 +309,29 @@ function resolvePrismaInvocation(rootDir) {
|
|
|
746
309
|
return { cmd: localBin, prefix: [] };
|
|
747
310
|
return { cmd: 'npx', prefix: ['prisma'] };
|
|
748
311
|
}
|
|
749
|
-
/** Where a start tries the migrations: next to the file, named for this process. */
|
|
750
|
-
const TRIAL_INFIX = '.xenon-trial-';
|
|
751
|
-
/** Beyond twice the file's size (the copy, and a migration that copies a table), for the copy. */
|
|
752
|
-
const TRIAL_MARGIN_BYTES = 64 * 1024 * 1024;
|
|
753
|
-
function removeTrialFiles(copy) {
|
|
754
|
-
for (const suffix of ['', '-journal', '-wal', '-shm']) {
|
|
755
|
-
fs.rmSync(`${copy}${suffix}`, { force: true });
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
/** Whether a process is running. Signal 0 only asks; it sends nothing. */
|
|
759
|
-
function processIsRunning(pid) {
|
|
760
|
-
if (!Number.isSafeInteger(pid) || pid <= 1)
|
|
761
|
-
return false;
|
|
762
|
-
try {
|
|
763
|
-
process.kill(pid, 0);
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
catch (err) {
|
|
767
|
-
return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
/** Removes the copies earlier starts left next to the file, unless their process still runs. */
|
|
771
|
-
function sweepTrialCopies(file) {
|
|
772
|
-
const dir = path.dirname(file);
|
|
773
|
-
const prefix = `${path.basename(file)}${TRIAL_INFIX}`;
|
|
774
|
-
let names;
|
|
775
|
-
try {
|
|
776
|
-
names = fs.readdirSync(dir);
|
|
777
|
-
}
|
|
778
|
-
catch (_a) {
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
for (const name of names) {
|
|
782
|
-
if (!name.startsWith(prefix))
|
|
783
|
-
continue;
|
|
784
|
-
const match = /^(\d+)(-journal|-wal|-shm)?$/.exec(name.slice(prefix.length));
|
|
785
|
-
if (!match)
|
|
786
|
-
continue;
|
|
787
|
-
const pid = Number(match[1]);
|
|
788
|
-
if (pid !== process.pid && processIsRunning(pid))
|
|
789
|
-
continue;
|
|
790
|
-
try {
|
|
791
|
-
fs.rmSync(path.join(dir, name), { force: true });
|
|
792
|
-
logger_1.default.info(`[DBMigrate] Removed ${name}, a copy of the database an earlier start left.`);
|
|
793
|
-
}
|
|
794
|
-
catch (err) {
|
|
795
|
-
logger_1.default.warn(`[DBMigrate] Could not remove ${name}, left by an earlier start: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
/**
|
|
800
|
-
* `migrate deploy` on a copy of the file, next to it (not in os.tmpdir(), which
|
|
801
|
-
* can be RAM-backed), with any failed migration rolled back there first, then
|
|
802
|
-
* whether the copy has everything prisma/schema.prisma has (`lacks`): the
|
|
803
|
-
* migrations make only what they make, so a copy they worked on can still lack
|
|
804
|
-
* what the tables lacked before them (an index dropped by hand, a column an
|
|
805
|
-
* older release's `db push` took away). The copy gets the file's own mode
|
|
806
|
-
* before any data is in it. It is skipped when there isn't room for it or the
|
|
807
|
-
* file can't be copied, and it is deleted whatever happens.
|
|
808
|
-
*/
|
|
809
|
-
function tryMigrationsOnCopy(file, failed, migrate, lacks, options) {
|
|
810
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
811
|
-
var _a, _b, _c, _d;
|
|
812
|
-
const copy = `${file}${TRIAL_INFIX}${process.pid}`;
|
|
813
|
-
let stats;
|
|
814
|
-
try {
|
|
815
|
-
stats = fs.statSync(file);
|
|
816
|
-
}
|
|
817
|
-
catch (err) {
|
|
818
|
-
return { outcome: 'skipped', why: 'copy', detail: String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err) };
|
|
819
|
-
}
|
|
820
|
-
const neededBytes = stats.size * 2 + TRIAL_MARGIN_BYTES;
|
|
821
|
-
const free = options.freeBytes(path.dirname(file));
|
|
822
|
-
if (free === null) {
|
|
823
|
-
logger_1.default.info('[DBMigrate] Xenon could not tell how much space is free next to the database, so it ' +
|
|
824
|
-
`tries the copy anyway (it needs about ${megabytes(neededBytes)} MB).`);
|
|
825
|
-
}
|
|
826
|
-
else if (free < neededBytes) {
|
|
827
|
-
return {
|
|
828
|
-
outcome: 'skipped',
|
|
829
|
-
why: 'space',
|
|
830
|
-
neededBytes,
|
|
831
|
-
detail: `the copy needs ${megabytes(neededBytes)} MB and ${megabytes(free)} MB is free`,
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
try {
|
|
835
|
-
removeTrialFiles(copy);
|
|
836
|
-
try {
|
|
837
|
-
// VACUUM INTO writes into an empty file, which keeps the mode it has.
|
|
838
|
-
fs.writeFileSync(copy, '', { mode: stats.mode & 0o777 });
|
|
839
|
-
fs.chmodSync(copy, stats.mode & 0o777);
|
|
840
|
-
yield options.copyDatabase(file, copy);
|
|
841
|
-
}
|
|
842
|
-
catch (err) {
|
|
843
|
-
const message = String((_d = (_c = (_b = err === null || err === void 0 ? void 0 : err.meta) === null || _b === void 0 ? void 0 : _b.message) !== null && _c !== void 0 ? _c : err === null || err === void 0 ? void 0 : err.message) !== null && _d !== void 0 ? _d : err).trim();
|
|
844
|
-
if (/disk is full|SQLITE_FULL|no space left|ENOSPC/i.test(message)) {
|
|
845
|
-
return { outcome: 'skipped', why: 'space', neededBytes, detail: message };
|
|
846
|
-
}
|
|
847
|
-
const locked = /database is locked|SQLITE_BUSY|busy/i.test(message);
|
|
848
|
-
return { outcome: 'skipped', why: locked ? 'locked' : 'copy', detail: message };
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
for (const name of failed) {
|
|
852
|
-
migrate(['migrate', 'resolve', '--rolled-back', name], `file:${copy}`);
|
|
853
|
-
}
|
|
854
|
-
migrate(['migrate', 'deploy'], `file:${copy}`);
|
|
855
|
-
}
|
|
856
|
-
catch (err) {
|
|
857
|
-
const output = cliOutput(err);
|
|
858
|
-
return { outcome: 'failed', output, migration: failedMigrationIn(output) };
|
|
859
|
-
}
|
|
860
|
-
const missing = lacks(`file:${copy}`);
|
|
861
|
-
return missing === null
|
|
862
|
-
? { outcome: 'worked' }
|
|
863
|
-
: { outcome: 'incomplete', stage: 'after', missing };
|
|
864
|
-
}
|
|
865
|
-
finally {
|
|
866
|
-
removeTrialFiles(copy);
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
/** A migration's SQL, as `migrationShape` reads it; unreadable counts as all unseen. */
|
|
871
|
-
function shapeIn(migrationsDir) {
|
|
872
|
-
const shapes = new Map();
|
|
873
|
-
return (name) => {
|
|
874
|
-
var _a;
|
|
875
|
-
let shape = shapes.get(name);
|
|
876
|
-
if (!shape) {
|
|
877
|
-
try {
|
|
878
|
-
shape = migrationShape(fs.readFileSync(path.join(migrationsDir, name, 'migration.sql'), 'utf8'));
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
shape = { statements: 2, unseen: [`(its SQL could not be read: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err})`] };
|
|
882
|
-
}
|
|
883
|
-
shapes.set(name, shape);
|
|
884
|
-
}
|
|
885
|
-
return shape;
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
312
|
/**
|
|
889
313
|
* Bring the database at `config.databaseUrl` up to date with prisma/schema.prisma,
|
|
890
314
|
* by the rule above. Throws an Error whose message says what to do when it can't.
|
|
891
315
|
* `npm run db:migrate` runs this too, whatever XENON_AUTO_MIGRATE says.
|
|
892
316
|
*/
|
|
893
317
|
function syncDatabaseSchema() {
|
|
894
|
-
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory
|
|
895
|
-
var _a
|
|
896
|
-
const rootDir =
|
|
318
|
+
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
|
|
319
|
+
var _a;
|
|
320
|
+
const rootDir = resolvePluginRoot();
|
|
897
321
|
const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
|
|
898
322
|
const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
|
|
899
323
|
if (!fs.existsSync(schemaPath) || !fs.existsSync(migrationsDir)) {
|
|
900
324
|
logger_1.default.warn(`[DBMigrate] Skipping auto-migrate: schema or migrations missing at ${rootDir}`);
|
|
901
|
-
return
|
|
325
|
+
return;
|
|
902
326
|
}
|
|
903
327
|
const databaseUrl = config_1.config.databaseUrl;
|
|
904
328
|
const where = (0, database_check_1.describeDatabaseUrl)(databaseUrl);
|
|
905
329
|
// The file the CLI will use: a relative path is read from prisma/, as Prisma does.
|
|
906
330
|
const file = sqliteFilePath(databaseUrl, path.dirname(schemaPath));
|
|
907
331
|
fs.mkdirSync(path.dirname(file !== null && file !== void 0 ? file : config_1.config.databasePath), { recursive: true });
|
|
908
|
-
if (file)
|
|
909
|
-
sweepTrialCopies(file);
|
|
910
332
|
const { cmd, prefix } = resolvePrismaInvocation(rootDir);
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
|
|
914
|
-
cwd: rootDir,
|
|
915
|
-
stdio: 'pipe',
|
|
916
|
-
});
|
|
333
|
+
const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL: databaseUrl });
|
|
334
|
+
const opts = { env, cwd: rootDir, stdio: 'pipe' };
|
|
917
335
|
let history;
|
|
918
336
|
try {
|
|
919
337
|
history = yield readHistory(file ? `file:${file}` : databaseUrl);
|
|
@@ -922,37 +340,22 @@ function syncDatabaseSchema() {
|
|
|
922
340
|
throw new Error('[DBMigrate] Cannot start: Xenon could not read the migration history of the database ' +
|
|
923
341
|
`at ${where}, so it can't tell how to bring it up to date: if another server is using ` +
|
|
924
342
|
'the file, stop that one; otherwise back the file up, check that it is a Xenon SQLite ' +
|
|
925
|
-
`database (Prisma's message is below), and start again.\n\n${(
|
|
343
|
+
`database (Prisma's message is below), and start again.\n\n${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
926
344
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
*/
|
|
931
|
-
const diffFromMigrations = (migrations, exitCode) => {
|
|
345
|
+
// Would the history's own migrations make exactly these tables? Built from
|
|
346
|
+
// this release's copies of the applied migrations, in a scratch directory.
|
|
347
|
+
const historyMatchesTables = (applied) => {
|
|
932
348
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-migration-history-'));
|
|
933
349
|
try {
|
|
934
350
|
const lock = path.join(migrationsDir, 'migration_lock.toml');
|
|
935
351
|
if (fs.existsSync(lock))
|
|
936
352
|
fs.copyFileSync(lock, path.join(scratch, 'migration_lock.toml'));
|
|
937
|
-
for (const name of
|
|
353
|
+
for (const name of applied) {
|
|
938
354
|
fs.mkdirSync(path.join(scratch, name));
|
|
939
355
|
fs.copyFileSync(path.join(migrationsDir, name, 'migration.sql'), path.join(scratch, name, 'migration.sql'));
|
|
940
356
|
}
|
|
941
357
|
const diff = ['migrate', 'diff', '--from-migrations', scratch, '--to-schema-datasource'];
|
|
942
|
-
|
|
943
|
-
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: databaseUrl }),
|
|
944
|
-
cwd: rootDir,
|
|
945
|
-
stdio: 'pipe',
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
finally {
|
|
949
|
-
fs.rmSync(scratch, { recursive: true, force: true });
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
// Would this run of migrations make exactly these tables?
|
|
953
|
-
const tablesMatch = (migrations) => {
|
|
954
|
-
try {
|
|
955
|
-
diffFromMigrations(migrations, true);
|
|
358
|
+
runSync(cmd, [...prefix, ...diff, schemaPath, '--exit-code'], opts);
|
|
956
359
|
return true;
|
|
957
360
|
}
|
|
958
361
|
catch (err) {
|
|
@@ -961,221 +364,61 @@ function syncDatabaseSchema() {
|
|
|
961
364
|
logger_1.default.warn(`[DBMigrate] Could not compare the tables with the migration history: ${cliOutput(err)}`);
|
|
962
365
|
return null;
|
|
963
366
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// them by hand aside); null when they lack nothing.
|
|
967
|
-
const tablesLack = (migrations) => {
|
|
968
|
-
var _a;
|
|
969
|
-
try {
|
|
970
|
-
return schemaMissingIn(String((_a = diffFromMigrations(migrations, false)) !== null && _a !== void 0 ? _a : ''));
|
|
971
|
-
}
|
|
972
|
-
catch (err) {
|
|
973
|
-
return `Xenon could not compare the tables with their migrations: ${cliOutput(err)}`;
|
|
974
|
-
}
|
|
975
|
-
};
|
|
976
|
-
// What prisma/schema.prisma has that a database lacks; null when it lacks nothing.
|
|
977
|
-
const lacksOfSchema = (url) => {
|
|
978
|
-
try {
|
|
979
|
-
const summary = runSync(cmd, [...prefix, 'migrate', 'diff', '--from-schema-datamodel', schemaPath, '--to-url', url], { env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }), cwd: rootDir, stdio: 'pipe' });
|
|
980
|
-
return schemaMissingIn(String(summary !== null && summary !== void 0 ? summary : ''));
|
|
981
|
-
}
|
|
982
|
-
catch (err) {
|
|
983
|
-
return `Xenon could not compare the copy with prisma/schema.prisma: ${cliOutput(err)}`;
|
|
367
|
+
finally {
|
|
368
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
984
369
|
}
|
|
985
370
|
};
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
file &&
|
|
991
|
-
plan.command === 'db push' &&
|
|
992
|
-
(plan.basis === 'tables-differ' || plan.basis === 'failed-migration'));
|
|
993
|
-
const at = {
|
|
994
|
-
prisma: [cmd, ...prefix].map(shellWord).join(' '),
|
|
995
|
-
schema: schemaPath,
|
|
996
|
-
databaseUrl: file ? `file:${file}` : databaseUrl,
|
|
997
|
-
file,
|
|
998
|
-
mode: file && fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : null,
|
|
999
|
-
migrationsDir,
|
|
1000
|
-
};
|
|
1001
|
-
if (plan.command === 'none') {
|
|
1002
|
-
logger_1.default.warn(`[DBMigrate] Not changing the database at ${where}: ${plan.reason}.`);
|
|
1003
|
-
throw new Error(schemaSyncFailure(plan, withoutStack((_d = (_c = plan.unresolved) === null || _c === void 0 ? void 0 : _c.logs) !== null && _d !== void 0 ? _d : ''), at));
|
|
1004
|
-
}
|
|
1005
|
-
const how = tryCopy ? 'migrate deploy on a copy first, else db push' : plan.command;
|
|
1006
|
-
const announce = `[DBMigrate] Syncing database schema (${how}) at ${where}: ${plan.reason}.`;
|
|
1007
|
-
// A database with a history that leaves plain migrate deploy is worth a look.
|
|
1008
|
-
if (history && (plan.command === 'db push' || plan.basis === 'rebaselined'))
|
|
371
|
+
const plan = planSchemaSync(history, localMigrationsIn(migrationsDir), historyMatchesTables);
|
|
372
|
+
const announce = `[DBMigrate] Syncing database schema (${plan.command}) at ${where}: ${plan.reason}.`;
|
|
373
|
+
// A database with a history that leaves migrate deploy is worth a look.
|
|
374
|
+
if (history && plan.command === 'db push')
|
|
1009
375
|
logger_1.default.warn(announce);
|
|
1010
376
|
else
|
|
1011
377
|
logger_1.default.info(announce);
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const leftNothing = !wasThere &&
|
|
1024
|
-
(shape.statements <= 1 || (shown && tablesMatch(localMigrations.slice(0, index)) === true));
|
|
1025
|
-
return { migration, wasThere, shown, leftNothing };
|
|
1026
|
-
};
|
|
1027
|
-
let step;
|
|
1028
|
-
let ran = plan;
|
|
378
|
+
const args = plan.command === 'db push'
|
|
379
|
+
? [
|
|
380
|
+
...prefix,
|
|
381
|
+
'db',
|
|
382
|
+
'push',
|
|
383
|
+
'--skip-generate',
|
|
384
|
+
...(plan.acceptDataLoss ? ['--accept-data-loss'] : []),
|
|
385
|
+
'--schema',
|
|
386
|
+
schemaPath,
|
|
387
|
+
]
|
|
388
|
+
: [...prefix, 'migrate', 'deploy', '--schema', schemaPath];
|
|
1029
389
|
try {
|
|
1030
|
-
|
|
1031
|
-
for (const name of (_f = (_e = plan.resolve) === null || _e === void 0 ? void 0 : _e.rolledBack) !== null && _f !== void 0 ? _f : []) {
|
|
1032
|
-
step = `migrate resolve --rolled-back ${name}`;
|
|
1033
|
-
logger_1.default.info(`[DBMigrate] Marking ${name} rolled back in the migration history.`);
|
|
1034
|
-
prisma(['migrate', 'resolve', '--rolled-back', name]);
|
|
1035
|
-
}
|
|
1036
|
-
for (const name of (_h = (_g = plan.resolve) === null || _g === void 0 ? void 0 : _g.applied) !== null && _h !== void 0 ? _h : []) {
|
|
1037
|
-
step = `migrate resolve --applied ${name}`;
|
|
1038
|
-
logger_1.default.info(`[DBMigrate] Recording ${name} as applied: the tables show all it does.`);
|
|
1039
|
-
prisma(['migrate', 'resolve', '--applied', name]);
|
|
1040
|
-
}
|
|
1041
|
-
step = undefined;
|
|
1042
|
-
prisma(['migrate', 'deploy']);
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
if (tryCopy && file && history) {
|
|
1046
|
-
// migrate deploy builds on what the recorded migrations made. On tables
|
|
1047
|
-
// that lack part of it (an index dropped by hand, a column an older
|
|
1048
|
-
// release's db push took away) it would leave the gap, or fill it
|
|
1049
|
-
// wrongly: SQLite reads a missing "column" in a redefined table's copy
|
|
1050
|
-
// as a string, so the column comes back holding its own name.
|
|
1051
|
-
const applied = new Set(history.applied);
|
|
1052
|
-
const lacking = tablesLack(localMigrations.filter((name) => applied.has(name)));
|
|
1053
|
-
let trial;
|
|
1054
|
-
if (lacking !== null) {
|
|
1055
|
-
trial = { outcome: 'incomplete', stage: 'before', missing: lacking };
|
|
1056
|
-
}
|
|
1057
|
-
else {
|
|
1058
|
-
let size = '';
|
|
1059
|
-
try {
|
|
1060
|
-
size = `, ${megabytes(fs.statSync(file).size)} MB to copy, which can take a while`;
|
|
1061
|
-
}
|
|
1062
|
-
catch (_l) {
|
|
1063
|
-
/* the trial says why */
|
|
1064
|
-
}
|
|
1065
|
-
logger_1.default.info("[DBMigrate] Trying this version's migrations on a copy of the database first " +
|
|
1066
|
-
`(${path.basename(file)}${TRIAL_INFIX}${process.pid}, next to it${size}).`);
|
|
1067
|
-
trial = yield tryMigrationsOnCopy(file, plan.failed, prisma, lacksOfSchema, {
|
|
1068
|
-
copyDatabase: (_j = options.copyDatabase) !== null && _j !== void 0 ? _j : copyWithVacuum,
|
|
1069
|
-
freeBytes: (_k = options.freeBytes) !== null && _k !== void 0 ? _k : freeBytesIn,
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
if (trial.outcome === 'worked') {
|
|
1073
|
-
logger_1.default.info("[DBMigrate] This version's migrations worked on the copy, and left it with " +
|
|
1074
|
-
'everything prisma/schema.prisma has, so Xenon runs prisma migrate deploy on the ' +
|
|
1075
|
-
'database.');
|
|
1076
|
-
ran = Object.assign(Object.assign({}, plan), { command: 'migrate deploy', trial });
|
|
1077
|
-
for (const name of plan.failed) {
|
|
1078
|
-
step = `migrate resolve --rolled-back ${name}`;
|
|
1079
|
-
prisma(['migrate', 'resolve', '--rolled-back', name]);
|
|
1080
|
-
}
|
|
1081
|
-
step = undefined;
|
|
1082
|
-
prisma(['migrate', 'deploy']);
|
|
1083
|
-
logger_1.default.info('[DBMigrate] Database schema in sync.');
|
|
1084
|
-
return ran;
|
|
1085
|
-
}
|
|
1086
|
-
logger_1.default.warn(trial.outcome === 'failed'
|
|
1087
|
-
? "[DBMigrate] This version's migrations failed on the copy" +
|
|
1088
|
-
(trial.migration ? ` (${trial.migration})` : '') +
|
|
1089
|
-
', so Xenon brings the database up to date with prisma db push.'
|
|
1090
|
-
: trial.outcome === 'incomplete' && trial.stage === 'before'
|
|
1091
|
-
? '[DBMigrate] The tables lack part of what their recorded migrations make, so ' +
|
|
1092
|
-
"Xenon doesn't try this version's migrations on them and brings them up to " +
|
|
1093
|
-
`date with prisma db push:\n${trial.missing}`
|
|
1094
|
-
: trial.outcome === 'incomplete'
|
|
1095
|
-
? "[DBMigrate] This version's migrations worked on the copy, but it still " +
|
|
1096
|
-
"lacked part of this version's schema, so Xenon doesn't run them on the " +
|
|
1097
|
-
`database and brings it up to date with prisma db push:\n${trial.missing}`
|
|
1098
|
-
: `[DBMigrate] Xenon did not try the migrations on a copy: ${trial.detail}.`);
|
|
1099
|
-
ran = Object.assign(Object.assign({}, plan), { trial });
|
|
1100
|
-
}
|
|
1101
|
-
prisma([
|
|
1102
|
-
'db',
|
|
1103
|
-
'push',
|
|
1104
|
-
'--skip-generate',
|
|
1105
|
-
...(plan.acceptDataLoss ? ['--accept-data-loss'] : []),
|
|
1106
|
-
]);
|
|
1107
|
-
}
|
|
390
|
+
runSync(cmd, args, opts);
|
|
1108
391
|
logger_1.default.info('[DBMigrate] Database schema in sync.');
|
|
1109
|
-
return ran;
|
|
1110
392
|
}
|
|
1111
393
|
catch (err) {
|
|
1112
394
|
const msg = cliOutput(err);
|
|
1113
|
-
logger_1.default.error(`[DBMigrate] prisma ${
|
|
395
|
+
logger_1.default.error(`[DBMigrate] prisma ${plan.command} failed: ${msg}`);
|
|
1114
396
|
// Boot must not continue. The schema is not what the code expects, so the
|
|
1115
397
|
// next query fails somewhere unrelated — `prisma.user.count()` reporting a
|
|
1116
398
|
// missing User table, say — and buries this, the actual cause. Stop here
|
|
1117
399
|
// and hand back something the operator can act on.
|
|
1118
|
-
|
|
1119
|
-
|
|
400
|
+
throw new Error(schemaSyncFailure(plan, msg, {
|
|
401
|
+
prisma: [cmd, ...prefix].map(shellWord).join(' '),
|
|
402
|
+
schema: schemaPath,
|
|
403
|
+
databaseUrl: file ? `file:${file}` : databaseUrl,
|
|
404
|
+
file,
|
|
405
|
+
}));
|
|
1120
406
|
}
|
|
1121
407
|
});
|
|
1122
408
|
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Whether prisma/schema.prisma has changes no migration in prisma/migrations
|
|
1125
|
-
* makes, with a warning when it has; null when it can't tell. `npm run
|
|
1126
|
-
* db:migrate` asks: a database with a migration history is updated with
|
|
1127
|
-
* `migrate deploy`, which applies migrations only, so it never gets them (nor
|
|
1128
|
-
* does any server this release reaches), while the log says it is in sync.
|
|
1129
|
-
* `npm run db:generate` gives the developer's database a history.
|
|
1130
|
-
*/
|
|
1131
|
-
function warnOfUnmigratedSchemaChanges(runSync = node_child_process_1.execFileSync, options = {}) {
|
|
1132
|
-
var _a;
|
|
1133
|
-
const rootDir = (_a = options.root) !== null && _a !== void 0 ? _a : resolvePluginRoot();
|
|
1134
|
-
const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
|
|
1135
|
-
const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
|
|
1136
|
-
if (!fs.existsSync(schemaPath) || !fs.existsSync(migrationsDir))
|
|
1137
|
-
return null;
|
|
1138
|
-
const { cmd, prefix } = resolvePrismaInvocation(rootDir);
|
|
1139
|
-
try {
|
|
1140
|
-
runSync(cmd, [
|
|
1141
|
-
...prefix,
|
|
1142
|
-
'migrate',
|
|
1143
|
-
'diff',
|
|
1144
|
-
'--from-migrations',
|
|
1145
|
-
migrationsDir,
|
|
1146
|
-
'--to-schema-datamodel',
|
|
1147
|
-
schemaPath,
|
|
1148
|
-
'--exit-code',
|
|
1149
|
-
], { env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl }), cwd: rootDir, stdio: 'pipe' });
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
catch (err) {
|
|
1153
|
-
if ((err === null || err === void 0 ? void 0 : err.status) === 2) {
|
|
1154
|
-
logger_1.default.warn('[DBMigrate] prisma/schema.prisma has changes that no migration in prisma/migrations ' +
|
|
1155
|
-
'makes. Add one with `npm run db:generate -- --name <change>`: a database with a ' +
|
|
1156
|
-
'migration history is brought up to date with prisma migrate deploy, which applies ' +
|
|
1157
|
-
'migrations only, so it never gets these changes, and nor does any server this ' +
|
|
1158
|
-
'release reaches, even where the log says the schema is in sync.');
|
|
1159
|
-
return true;
|
|
1160
|
-
}
|
|
1161
|
-
logger_1.default.warn(`[DBMigrate] Could not compare prisma/schema.prisma with prisma/migrations: ${cliOutput(err)}`);
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
409
|
/** The startup step: `syncDatabaseSchema`, unless XENON_AUTO_MIGRATE=false. */
|
|
1166
410
|
function runMigrations() {
|
|
1167
|
-
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory
|
|
411
|
+
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
|
|
1168
412
|
if (!config_1.config.autoMigrate) {
|
|
1169
413
|
logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. Bring the schema up ' +
|
|
1170
414
|
'to date yourself before the server starts: `prisma migrate deploy` for a database ' +
|
|
1171
415
|
'whose migration history (`_prisma_migrations`) matches its tables (`prisma migrate ' +
|
|
1172
416
|
'diff --from-migrations <the recorded ones> --to-schema-datasource prisma/schema.prisma ' +
|
|
1173
417
|
'--exit-code` says so), `prisma db push` for any other (a `migrate deploy` that stops ' +
|
|
1174
|
-
'with P3018 on a change already there means the database is one of those,
|
|
1175
|
-
'`
|
|
1176
|
-
'or `npm run db:migrate` from a source checkout, which chooses for you.');
|
|
418
|
+
'with P3018 on a change already there means the database is one of those), or ' +
|
|
419
|
+
'`npm run db:migrate` from a source checkout, which chooses for you.');
|
|
1177
420
|
return;
|
|
1178
421
|
}
|
|
1179
|
-
yield syncDatabaseSchema(runSync, readHistory
|
|
422
|
+
yield syncDatabaseSchema(runSync, readHistory);
|
|
1180
423
|
});
|
|
1181
424
|
}
|