mysqlsyncr 1.1.10 → 1.2.1
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 +246 -186
- package/__tests__/acceptCurrent.test.js +117 -0
- package/__tests__/columnAttributes.test.js +101 -0
- package/__tests__/options.test.js +259 -0
- package/__tests__/parser.test.js +167 -166
- package/__tests__/parser2.test.js +155 -152
- package/__tests__/paths.test.js +21 -0
- package/__tests__/prompt.test.js +106 -0
- package/__tests__/sync.test.js +348 -0
- package/__tests__/templateDrift.test.js +72 -0
- package/__tests__/versions.test.js +138 -0
- package/dist/index.cjs +4070 -1730
- package/dist/index.js +4106 -1768
- package/package.json +66 -59
- package/rollup.config.js +25 -24
- package/src/DBStructureChecker.js +47 -0
- package/src/acceptCurrent.js +225 -0
- package/src/apply.js +481 -348
- package/src/cli.js +61 -34
- package/src/compare.js +106 -92
- package/src/differences.js +258 -212
- package/src/dump.js +63 -54
- package/src/getters.js +151 -139
- package/src/index.js +223 -170
- package/src/loggers.js +156 -139
- package/src/normalizers.js +28 -6
- package/src/options.js +133 -0
- package/src/parsers.js +197 -145
- package/src/paths.js +41 -0
- package/src/prompt.js +109 -0
- package/src/rebuild.js +106 -0
- package/src/rows.js +46 -0
- package/src/socketServer.js +311 -229
- package/src/templateDrift.js +93 -0
- package/src/versions.js +112 -0
- package/ui/README.md +55 -40
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
- package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
- package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
- package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
- package/ui/dist/spa/index.html +2 -2
- package/ui/package.json +43 -42
- package/ui/src/components/DefinitionDiff.vue +169 -0
- package/ui/src/pages/MainComponent.vue +704 -78
- package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
- package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
- package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
- package/ui/dist/spa/assets/index-kXpA8n8r.css +0 -5
package/src/apply.js
CHANGED
|
@@ -1,348 +1,481 @@
|
|
|
1
|
-
import { logger } from './loggers.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
console.log(`
|
|
312
|
-
await
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
1
|
+
import { logger } from './loggers.js';
|
|
2
|
+
import { adaptSQLForServer, getServerVersion, portableCollation } from './versions.js';
|
|
3
|
+
|
|
4
|
+
const quoteString = (value) => `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
5
|
+
|
|
6
|
+
// Types MySQL won't give a literal default
|
|
7
|
+
const noLiteralDefaultRegex = /^(tiny|medium|long)?(text|blob)$|^json$|^geometry|^point$|^linestring$|^polygon$/i;
|
|
8
|
+
|
|
9
|
+
const defaultClause = (field) => {
|
|
10
|
+
const value = field.Default;
|
|
11
|
+
if (value === undefined || value === null || value === 'NULL') {
|
|
12
|
+
// A NOT NULL column can't default to NULL; leave it without a default
|
|
13
|
+
return field.NotNull ? '' : 'DEFAULT NULL';
|
|
14
|
+
}
|
|
15
|
+
// Functions, expressions, and bit/hex literals such as b'0' are written as they were read
|
|
16
|
+
if (/^(CURRENT_TIMESTAMP|now)(\(\d*\))?$/i.test(value) || value.startsWith('(') || /^[bx]'[0-9a-f]*'$/i.test(value) || /^0x[0-9a-f]+$/i.test(value)) {
|
|
17
|
+
return `DEFAULT ${value}`;
|
|
18
|
+
}
|
|
19
|
+
if (noLiteralDefaultRegex.test(field.Type)) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
return `DEFAULT ${quoteString(value)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Template value for a column detail, or the live column's when the template doesn't record it
|
|
26
|
+
const columnDetail = (field, currentField, key) => key in field ? field[key] : currentField?.[key];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Full column definition for ADD/CHANGE, e.g.
|
|
30
|
+
* timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
31
|
+
* NULL/NOT NULL is always explicit: without it a timestamp is NOT NULL when
|
|
32
|
+
* explicit_defaults_for_timestamp is OFF, and DEFAULT NULL then fails.
|
|
33
|
+
* Charset, collation, comment, ON UPDATE and a generated column's expression
|
|
34
|
+
* come from the template when it has them and are otherwise kept from the live
|
|
35
|
+
* column, since CHANGE drops anything not restated.
|
|
36
|
+
*/
|
|
37
|
+
export const columnDefinition = (field, currentField) => {
|
|
38
|
+
const charset = columnDetail(field, currentField, 'Charset');
|
|
39
|
+
const collate = columnDetail(field, currentField, 'Collate');
|
|
40
|
+
const onUpdate = columnDetail(field, currentField, 'OnUpdate');
|
|
41
|
+
const comment = columnDetail(field, currentField, 'Comment');
|
|
42
|
+
const generated = columnDetail(field, currentField, 'Generated');
|
|
43
|
+
if (generated) {
|
|
44
|
+
// A generated column takes no DEFAULT, ON UPDATE or AUTO_INCREMENT
|
|
45
|
+
return [
|
|
46
|
+
field.Type,
|
|
47
|
+
charset ? `CHARACTER SET ${charset}` : '',
|
|
48
|
+
collate ? `COLLATE ${collate}` : '',
|
|
49
|
+
`GENERATED ALWAYS AS ${generated}`,
|
|
50
|
+
field.NotNull ? 'NOT NULL' : 'NULL',
|
|
51
|
+
comment ? `COMMENT ${quoteString(comment)}` : ''
|
|
52
|
+
].filter(Boolean).join(' ');
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
field.Type,
|
|
56
|
+
charset ? `CHARACTER SET ${charset}` : '',
|
|
57
|
+
collate ? `COLLATE ${collate}` : '',
|
|
58
|
+
field.NotNull ? 'NOT NULL' : 'NULL',
|
|
59
|
+
defaultClause(field),
|
|
60
|
+
onUpdate ? `ON UPDATE ${onUpdate}` : '',
|
|
61
|
+
field.AutoIncrement ? 'AUTO_INCREMENT' : '',
|
|
62
|
+
comment ? `COMMENT ${quoteString(comment)}` : ''
|
|
63
|
+
].filter(Boolean).join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// `col`, or `col`(255) for a prefix index on a text column
|
|
67
|
+
const formatIndexColumns = (columns) => columns.map(col => {
|
|
68
|
+
const match = col.match(/^`?([^`(]+)`?(?:\((\d+)\))?$/);
|
|
69
|
+
if (!match) return `\`${col}\``;
|
|
70
|
+
return `\`${match[1]}\`${match[2] ? `(${match[2]})` : ''}`;
|
|
71
|
+
}).join(', ');
|
|
72
|
+
|
|
73
|
+
const isPrimaryIndex = (index) => !!index.Primary || index.Name === 'PRIMARY';
|
|
74
|
+
|
|
75
|
+
const addIndexClause = (index) => isPrimaryIndex(index)
|
|
76
|
+
? `ADD PRIMARY KEY (${formatIndexColumns(index.ColumnName)})`
|
|
77
|
+
: `ADD ${index.Unique ? 'UNIQUE ' : ''}INDEX \`${index.Name}\` (${formatIndexColumns(index.ColumnName)})`;
|
|
78
|
+
|
|
79
|
+
const dropIndexClause = (index) => isPrimaryIndex(index) ? 'DROP PRIMARY KEY' : `DROP INDEX \`${index.Name}\``;
|
|
80
|
+
|
|
81
|
+
const describeDifference = (diff) => {
|
|
82
|
+
const name = diff.tableName || diff.viewName || diff.Name || diff.database || '';
|
|
83
|
+
const detail = diff.field?.Field || diff.indexName || diff.trigger?.Name || (diff.key ? JSON.stringify(diff.key) : '');
|
|
84
|
+
return `${diff.type} ${name}${detail ? '.' + detail : ''}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Rewrites one collation in a CREATE TABLE, both as a table option (COLLATE=x) and as a
|
|
88
|
+
// column attribute (COLLATE x). Passing null for `to` removes the clause entirely.
|
|
89
|
+
const replaceCollation = (sql, from, to) => {
|
|
90
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
return sql
|
|
92
|
+
.replace(new RegExp('COLLATE(\\s*)=(\\s*)' + escaped, 'gi'), to ? `COLLATE$1=$2${to}` : '')
|
|
93
|
+
.replace(new RegExp('COLLATE(\\s+)' + escaped, 'gi'), to ? `COLLATE$1${to}` : '');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Rewrites that let DDL dumped from a newer server run on an older one. Each inspects the
|
|
97
|
+
// error the server actually returned and returns the amended SQL plus what changed, or
|
|
98
|
+
// null when it has nothing to offer - so a rewrite only ever fires for the failure it
|
|
99
|
+
// was written for, and anything else still surfaces as a failure. These catch what
|
|
100
|
+
// adaptSQLForServer can't anticipate, e.g. when the server's version couldn't be read.
|
|
101
|
+
const compatRewrites = [
|
|
102
|
+
(sql, errorMessage) => {
|
|
103
|
+
const unknown = /Unknown collation: '([^']+)'/i.exec(errorMessage);
|
|
104
|
+
if (!unknown) return null;
|
|
105
|
+
const substitute = portableCollation(unknown[1]);
|
|
106
|
+
const rewritten = replaceCollation(sql, unknown[1], substitute);
|
|
107
|
+
if (rewritten === sql) return null;
|
|
108
|
+
return {
|
|
109
|
+
sql: rewritten,
|
|
110
|
+
message: `server does not support collation ${unknown[1]}, created with ${substitute || 'the charset default'} instead`,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
(sql, errorMessage) => {
|
|
114
|
+
// Index visibility is MySQL 8.0.23+. Only strip the keyword where it is legal -
|
|
115
|
+
// directly after an index definition's closing parenthesis - so the word cannot
|
|
116
|
+
// be removed from a column comment or a column named `visible`.
|
|
117
|
+
if (!/error in your SQL syntax/i.test(errorMessage) || !/\b(?:IN)?VISIBLE\b/i.test(errorMessage)) return null;
|
|
118
|
+
const rewritten = sql.replace(/\)(\s+)(?:IN)?VISIBLE(?=\s*[,)\r\n])/g, ')');
|
|
119
|
+
if (rewritten === sql) return null;
|
|
120
|
+
return {
|
|
121
|
+
sql: rewritten,
|
|
122
|
+
message: 'server does not support index visibility, created with every index visible',
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Apply the differences in order. A failing difference doesn't stop the rest.
|
|
129
|
+
* Returns { attempted, failures, warnings }: failures are the differences that
|
|
130
|
+
* couldn't be applied, warnings the ones applied with a change for this server
|
|
131
|
+
* (e.g. an 8.0 collation on 5.7), each as { type, tableName, description, message }.
|
|
132
|
+
*/
|
|
133
|
+
export const applyDifferences = async (connection, database, differences) => {
|
|
134
|
+
const failures = [];
|
|
135
|
+
const warnings = [];
|
|
136
|
+
let current = null; // the difference being applied, for warnings from query()
|
|
137
|
+
const warn = (diff, message) => {
|
|
138
|
+
console.warn(`Applied ${describeDifference(diff)} with a change: ${message}`);
|
|
139
|
+
warnings.push({ type: diff.type, tableName: diff.tableName, description: describeDifference(diff), message });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Template SQL from MySQL 8.0 can name collations a 5.7 server doesn't have
|
|
143
|
+
const serverVersion = await getServerVersion(connection);
|
|
144
|
+
const query = async (sql, values) => {
|
|
145
|
+
const adapted = adaptSQLForServer(sql, serverVersion);
|
|
146
|
+
if (adapted !== sql) {
|
|
147
|
+
console.log(`Adapted for MySQL ${serverVersion}: ${adapted}`);
|
|
148
|
+
if (current) warn(current, `MySQL ${serverVersion} has no utf8mb4_0900 collations, so the closest one it has was used`);
|
|
149
|
+
}
|
|
150
|
+
return connection.query(adapted, values);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// A dump written by MySQL 8 assumes explicit_defaults_for_timestamp is on, where a
|
|
154
|
+
// `timestamp NOT NULL` column with no DEFAULT is just a column with no default. With
|
|
155
|
+
// the MySQL 5.7 default of off, the server instead assigns every TIMESTAMP NOT NULL
|
|
156
|
+
// column after the first an implicit '0000-00-00 00:00:00', which strict mode then
|
|
157
|
+
// rejects at CREATE TABLE time. Matching MySQL 8 for this session applies that DDL as
|
|
158
|
+
// written; DDL that states its defaults - everything SHOW CREATE TABLE emits, on
|
|
159
|
+
// either version - parses the same way with the setting on or off.
|
|
160
|
+
try {
|
|
161
|
+
await connection.query('SET SESSION explicit_defaults_for_timestamp = 1');
|
|
162
|
+
} catch (e) {
|
|
163
|
+
logger(`Could not set explicit_defaults_for_timestamp for this session: ${e.message}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
differences.sort((a, b) => {
|
|
167
|
+
//Sort to make sure we apply the differences in a logical order.
|
|
168
|
+
const typePriority = {
|
|
169
|
+
'missing_database': 1,
|
|
170
|
+
'missing_table': 2,
|
|
171
|
+
'extra_table': 2,
|
|
172
|
+
'missing_field': 3,
|
|
173
|
+
'extra_field': 3,
|
|
174
|
+
'mismatched_field': 3,
|
|
175
|
+
'missing_index': 4,
|
|
176
|
+
'extra_index': 4,
|
|
177
|
+
'mismatched_index': 4,
|
|
178
|
+
'missing_procedure': 5,
|
|
179
|
+
'mismatched_procedure': 5,
|
|
180
|
+
'missing_view': 6,
|
|
181
|
+
'mismatched_view': 6,
|
|
182
|
+
'extra_view': 6,
|
|
183
|
+
'missing_trigger': 7,
|
|
184
|
+
'mismatched_trigger': 7,
|
|
185
|
+
'extra_trigger': 7,
|
|
186
|
+
// after the table and its columns exist
|
|
187
|
+
'missing_row': 8
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (typePriority[a.type] || 999) - (typePriority[b.type] || 999);
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
for (const diff of differences) {
|
|
195
|
+
current = diff;
|
|
196
|
+
try {
|
|
197
|
+
|
|
198
|
+
if(diff.type == 'missing_database'){
|
|
199
|
+
const createDatabaseSQL = `CREATE DATABASE \`${diff.database}\`;`;
|
|
200
|
+
console.log(`Executing: ${createDatabaseSQL}`);
|
|
201
|
+
await query(createDatabaseSQL);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
await query(`USE \`${database}\``);
|
|
207
|
+
if (diff.type === 'missing_table') {
|
|
208
|
+
const { tableName, createSQL } = diff;
|
|
209
|
+
logger(`Creating missing table ${tableName}`);
|
|
210
|
+
console.log(`Executing: ${createSQL}`);
|
|
211
|
+
try {
|
|
212
|
+
await query(createSQL);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// The dump can name things this server has never heard of - a MySQL 8
|
|
215
|
+
// collation, index visibility. Amend the statement for whatever the
|
|
216
|
+
// server objected to and retry, one objection at a time, since it only
|
|
217
|
+
// reports the first. The table then exists, but not exactly as dumped,
|
|
218
|
+
// so record every change rather than calling it a clean success.
|
|
219
|
+
let sql = createSQL;
|
|
220
|
+
let error = e;
|
|
221
|
+
const changes = [];
|
|
222
|
+
|
|
223
|
+
while (error && changes.length < compatRewrites.length) {
|
|
224
|
+
const message = error.message;
|
|
225
|
+
const rewrite = compatRewrites.reduce((found, fn) => found || fn(sql, message), null);
|
|
226
|
+
if (!rewrite) break;
|
|
227
|
+
|
|
228
|
+
sql = rewrite.sql;
|
|
229
|
+
changes.push(rewrite.message);
|
|
230
|
+
console.log(`Retrying: ${sql}`);
|
|
231
|
+
try {
|
|
232
|
+
await query(sql);
|
|
233
|
+
error = null;
|
|
234
|
+
} catch (retryError) {
|
|
235
|
+
error = retryError;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (error) throw error;
|
|
240
|
+
warn(diff, changes.join('; '));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
} else if (diff.type === 'extra_table') {
|
|
244
|
+
const { tableName } = diff;
|
|
245
|
+
logger(`Dropping extra table ${tableName}`);
|
|
246
|
+
const dropTableSQL = `DROP TABLE \`${tableName}\`;`;
|
|
247
|
+
console.log(`Executing: ${dropTableSQL}`);
|
|
248
|
+
await query(dropTableSQL);
|
|
249
|
+
|
|
250
|
+
} else if (diff.type === 'missing_field') {
|
|
251
|
+
// Existing rows get the column's default, or the type's implicit default for NOT NULL without one
|
|
252
|
+
const addFieldSQL = `ALTER TABLE \`${diff.tableName}\` ADD COLUMN \`${diff.field.Field}\` ${columnDefinition(diff.field)};`;
|
|
253
|
+
console.log(`Executing: ${addFieldSQL}`);
|
|
254
|
+
await query(addFieldSQL);
|
|
255
|
+
|
|
256
|
+
} else if (diff.type === 'extra_field') {
|
|
257
|
+
const dropFieldSQL = `ALTER TABLE \`${diff.tableName}\` DROP COLUMN \`${diff.field.Field}\`;`;
|
|
258
|
+
console.log(`Executing: ${dropFieldSQL}`);
|
|
259
|
+
await query(dropFieldSQL);
|
|
260
|
+
|
|
261
|
+
} else if (diff.type === 'mismatched_field') {
|
|
262
|
+
const { tableName, field, currentField } = diff;
|
|
263
|
+
|
|
264
|
+
// use alter table rather than adding fields
|
|
265
|
+
|
|
266
|
+
// // Helper function to determine if we need batch processing
|
|
267
|
+
// const needsBatchProcessing = () => {
|
|
268
|
+
// const currentType = currentField.Type.toLowerCase();
|
|
269
|
+
// const newType = field.Type.toLowerCase();
|
|
270
|
+
|
|
271
|
+
// // Decimal changes
|
|
272
|
+
// if (currentType.includes('decimal') && newType.includes('decimal')) {
|
|
273
|
+
// return true;
|
|
274
|
+
// }
|
|
275
|
+
|
|
276
|
+
// // VARCHAR/CHAR length reductions
|
|
277
|
+
// if ((currentType.includes('varchar') || currentType.includes('char')) &&
|
|
278
|
+
// (newType.includes('varchar') || newType.includes('char'))) {
|
|
279
|
+
// const currentLength = parseInt(currentType.match(/\((\d+)\)/)?.[1] || '0');
|
|
280
|
+
// const newLength = parseInt(newType.match(/\((\d+)\)/)?.[1] || '0');
|
|
281
|
+
// return newLength < currentLength;
|
|
282
|
+
// }
|
|
283
|
+
|
|
284
|
+
// // TEXT to VARCHAR conversions
|
|
285
|
+
// if (currentType.includes('text') && newType.includes('varchar')) {
|
|
286
|
+
// return true;
|
|
287
|
+
// }
|
|
288
|
+
|
|
289
|
+
// // Numeric type downgrades
|
|
290
|
+
// const numericDowngrades = {
|
|
291
|
+
// 'bigint': ['int', 'mediumint', 'smallint', 'tinyint'],
|
|
292
|
+
// 'int': ['mediumint', 'smallint', 'tinyint'],
|
|
293
|
+
// 'mediumint': ['smallint', 'tinyint'],
|
|
294
|
+
// 'smallint': ['tinyint'],
|
|
295
|
+
// 'double': ['float', 'decimal'],
|
|
296
|
+
// 'float': ['decimal']
|
|
297
|
+
// };
|
|
298
|
+
|
|
299
|
+
// for (const [larger, smaller] of Object.entries(numericDowngrades)) {
|
|
300
|
+
// if (currentType.includes(larger) && smaller.some(t => newType.includes(t))) {
|
|
301
|
+
// return true;
|
|
302
|
+
// }
|
|
303
|
+
// }
|
|
304
|
+
|
|
305
|
+
// return false;
|
|
306
|
+
// };
|
|
307
|
+
|
|
308
|
+
// if (needsBatchProcessing()) {
|
|
309
|
+
// // Step 1: Add new column
|
|
310
|
+
// const addColumnSQL = `ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.Field}_new\` ${field.Type} DEFAULT NULL;`;
|
|
311
|
+
// console.log(`Adding new column for type conversion: ${addColumnSQL}`);
|
|
312
|
+
// await query(addColumnSQL);
|
|
313
|
+
|
|
314
|
+
// // Step 2: Copy data in batches
|
|
315
|
+
// let totalUpdated = 0;
|
|
316
|
+
// let batchSize = 10000;
|
|
317
|
+
// let batchCount = 0;
|
|
318
|
+
|
|
319
|
+
// while (true) {
|
|
320
|
+
// const updateSQL = `
|
|
321
|
+
// UPDATE \`${tableName}\`
|
|
322
|
+
// SET \`${field.Field}_new\` = \`${field.Field}\`
|
|
323
|
+
// WHERE \`${field.Field}_new\` IS NULL
|
|
324
|
+
// AND \`${field.Field}\` IS NOT NULL
|
|
325
|
+
// LIMIT ${batchSize};
|
|
326
|
+
// `;
|
|
327
|
+
// const result = await query(updateSQL);
|
|
328
|
+
// const rowsAffected = result.affectedRows;
|
|
329
|
+
|
|
330
|
+
// if (rowsAffected === 0) break;
|
|
331
|
+
|
|
332
|
+
// totalUpdated += rowsAffected;
|
|
333
|
+
// batchCount++;
|
|
334
|
+
// console.log(`Batch ${batchCount}: Converted ${rowsAffected} rows. Total converted: ${totalUpdated}`);
|
|
335
|
+
// }
|
|
336
|
+
|
|
337
|
+
// // Step 3: Verify data (optional)
|
|
338
|
+
// const verifySQL = `
|
|
339
|
+
// SELECT COUNT(*) as mismatch FROM \`${tableName}\`
|
|
340
|
+
// WHERE \`${field.Field}\` IS NOT NULL
|
|
341
|
+
// AND \`${field.Field}_new\` IS NULL;
|
|
342
|
+
// `;
|
|
343
|
+
// const [verifyResult] = await query(verifySQL);
|
|
344
|
+
// if (verifyResult.mismatch > 0) {
|
|
345
|
+
// console.log(`Warning: ${verifyResult.mismatch} rows may have failed conversion`);
|
|
346
|
+
// }
|
|
347
|
+
|
|
348
|
+
// // Step 4: Swap columns
|
|
349
|
+
// const swapColumnsSQL = `
|
|
350
|
+
// ALTER TABLE \`${tableName}\`
|
|
351
|
+
// DROP COLUMN \`${field.Field}\`,
|
|
352
|
+
// RENAME COLUMN \`${field.Field}_new\` TO \`${field.Field}\`
|
|
353
|
+
// `;
|
|
354
|
+
// console.log(`Executing: ${swapColumnsSQL}`);
|
|
355
|
+
// await query(swapColumnsSQL);
|
|
356
|
+
|
|
357
|
+
// console.log(`Completed type conversion for ${field.Field}. Total rows converted: ${totalUpdated}`);
|
|
358
|
+
// } else {
|
|
359
|
+
// Original mismatched field handling for simple changes
|
|
360
|
+
const modifyFieldSQL = `ALTER TABLE \`${tableName}\` CHANGE \`${field.Field}\` \`${field.Field}\` ${columnDefinition(field, currentField)};`;
|
|
361
|
+
console.log(`Executing: ${modifyFieldSQL}`);
|
|
362
|
+
await query(modifyFieldSQL);
|
|
363
|
+
// }
|
|
364
|
+
|
|
365
|
+
} else if (diff.type === 'missing_index') {
|
|
366
|
+
const existingColumns = await query(`SHOW COLUMNS FROM \`${diff.tableName}\``);
|
|
367
|
+
const columnNames = existingColumns.map(col => col.Field);
|
|
368
|
+
|
|
369
|
+
// Check if all columns for the index exist
|
|
370
|
+
const missingColumns = Array.isArray(diff.index.ColumnName) ? diff.index.ColumnName.filter(col => !columnNames.includes(col) && !columnNames.includes(col.split("(")?.[0])) : [];
|
|
371
|
+
|
|
372
|
+
if (missingColumns.length > 0) {
|
|
373
|
+
throw new Error(`Cannot create index ${diff.index.Name} on ${diff.tableName}. Missing columns: ${missingColumns.join(', ')}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!Array.isArray(diff.index.ColumnName) || diff.index.ColumnName.length === 0) {
|
|
377
|
+
throw new Error(`Cannot create index ${diff.index.Name} on ${diff.tableName}. ColumnName is undefined or empty.`);
|
|
378
|
+
}
|
|
379
|
+
const addIndexSQL = `ALTER TABLE \`${diff.tableName}\` ${addIndexClause(diff.index)};`;
|
|
380
|
+
console.log(`Executing: ${addIndexSQL}`);
|
|
381
|
+
await query(addIndexSQL);
|
|
382
|
+
|
|
383
|
+
} else if (diff.type === 'extra_index') {
|
|
384
|
+
const dropIndexSQL = `ALTER TABLE \`${diff.tableName}\` ${dropIndexClause(diff.index)};`;
|
|
385
|
+
console.log(`Executing: ${dropIndexSQL}`);
|
|
386
|
+
await query(dropIndexSQL);
|
|
387
|
+
|
|
388
|
+
} else if (diff.type === 'mismatched_index') {
|
|
389
|
+
const { tableName, index } = diff;
|
|
390
|
+
// Drop and re-add in one statement: the index is never missing if the add fails,
|
|
391
|
+
// and a primary key on an AUTO_INCREMENT column can be replaced
|
|
392
|
+
const replaceIndexSQL = `ALTER TABLE \`${tableName}\` ${dropIndexClause(index)}, ${addIndexClause(index)};`;
|
|
393
|
+
console.log(`Executing: ${replaceIndexSQL}`);
|
|
394
|
+
await query(replaceIndexSQL);
|
|
395
|
+
|
|
396
|
+
} else if (diff.type === 'missing_procedure' || diff.type === 'mismatched_procedure') {
|
|
397
|
+
|
|
398
|
+
// Drop the procedure if it exists
|
|
399
|
+
const dropCurrentProcSQL = `DROP PROCEDURE IF EXISTS \`${diff.Name}\`;`;
|
|
400
|
+
console.log(`Executing: ${dropCurrentProcSQL}`);
|
|
401
|
+
await query(dropCurrentProcSQL);
|
|
402
|
+
|
|
403
|
+
// Create the procedure
|
|
404
|
+
const createProcSQL = diff.Definition;
|
|
405
|
+
console.log(`Executing: ${createProcSQL}`); // Log the SQL statement
|
|
406
|
+
await query(createProcSQL);
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
} else if (diff.type === 'missing_view') {
|
|
411
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
412
|
+
console.log(`Executing: ${dropViewSQL}`);
|
|
413
|
+
await query(dropViewSQL);
|
|
414
|
+
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
415
|
+
console.log(`Executing: ${createViewSQL}`);
|
|
416
|
+
await query(createViewSQL);
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
} else if (diff.type === 'mismatched_view') {
|
|
420
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
421
|
+
console.log(`Executing: ${dropViewSQL}`);
|
|
422
|
+
await query(dropViewSQL);
|
|
423
|
+
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
424
|
+
console.log(`Executing: ${createViewSQL}`);
|
|
425
|
+
await query(createViewSQL);
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
} else if (diff.type === 'extra_view') {
|
|
429
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
430
|
+
console.log(`Executing: ${dropViewSQL}`);
|
|
431
|
+
await query(dropViewSQL);
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
} else if (diff.type === 'missing_trigger') {
|
|
435
|
+
const createTriggerSQL = diff.trigger.Definition;
|
|
436
|
+
logger('fixing trigger', diff.trigger.Name)
|
|
437
|
+
console.log(`Executing: ${createTriggerSQL}`);
|
|
438
|
+
await query(createTriggerSQL);
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
} else if (diff.type === 'mismatched_trigger') {
|
|
442
|
+
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
443
|
+
console.log(`Executing: ${dropTriggerSQL}`);
|
|
444
|
+
await query(dropTriggerSQL);
|
|
445
|
+
const createTriggerSQL = diff.trigger.Definition;
|
|
446
|
+
console.log(`Executing: ${createTriggerSQL}`);
|
|
447
|
+
await query(createTriggerSQL);
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
} else if (diff.type === 'extra_trigger') {
|
|
451
|
+
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
452
|
+
console.log(`Executing: ${dropTriggerSQL}`);
|
|
453
|
+
await query(dropTriggerSQL);
|
|
454
|
+
|
|
455
|
+
} else if (diff.type === 'missing_row') {
|
|
456
|
+
const columns = Object.keys(diff.row);
|
|
457
|
+
const insertRowSQL = `INSERT INTO \`${diff.tableName}\` (${columns.map(col => `\`${col}\``).join(', ')}) VALUES (${columns.map(() => '?').join(', ')});`;
|
|
458
|
+
console.log(`Executing: ${insertRowSQL}`, JSON.stringify(diff.row));
|
|
459
|
+
await query(insertRowSQL, columns.map(col => diff.row[col]));
|
|
460
|
+
|
|
461
|
+
}else if(diff.type === 'missing_database'){
|
|
462
|
+
logger('fixing missing database', diff.database)
|
|
463
|
+
const createDatabaseSQL = `CREATE DATABASE \`${diff.database}\`;`;
|
|
464
|
+
console.log(`Executing: ${createDatabaseSQL}`);
|
|
465
|
+
await query(createDatabaseSQL);
|
|
466
|
+
} else {
|
|
467
|
+
throw new Error(`Unknown difference type: ${diff.type}`);
|
|
468
|
+
}
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.error(`Failed to apply ${describeDifference(diff)}: ${e.message}`);
|
|
471
|
+
logger(diff);
|
|
472
|
+
failures.push({ type: diff.type, tableName: diff.tableName, description: describeDifference(diff), message: e.message });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { attempted: differences.length, failures, warnings };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// One line summing up the failures from applyDifferences, for errors and logs
|
|
480
|
+
export const describeFailures = ({ attempted, failures }) =>
|
|
481
|
+
`${failures.length} of ${attempted} difference(s) failed to apply: ${failures.map(f => `${f.description} (${f.message})`).join('; ')}`;
|