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.
Files changed (62) hide show
  1. package/README.md +246 -186
  2. package/__tests__/acceptCurrent.test.js +117 -0
  3. package/__tests__/columnAttributes.test.js +101 -0
  4. package/__tests__/options.test.js +259 -0
  5. package/__tests__/parser.test.js +167 -166
  6. package/__tests__/parser2.test.js +155 -152
  7. package/__tests__/paths.test.js +21 -0
  8. package/__tests__/prompt.test.js +106 -0
  9. package/__tests__/sync.test.js +348 -0
  10. package/__tests__/templateDrift.test.js +72 -0
  11. package/__tests__/versions.test.js +138 -0
  12. package/dist/index.cjs +4070 -1730
  13. package/dist/index.js +4106 -1768
  14. package/package.json +66 -59
  15. package/rollup.config.js +25 -24
  16. package/src/DBStructureChecker.js +47 -0
  17. package/src/acceptCurrent.js +225 -0
  18. package/src/apply.js +481 -348
  19. package/src/cli.js +61 -34
  20. package/src/compare.js +106 -92
  21. package/src/differences.js +258 -212
  22. package/src/dump.js +63 -54
  23. package/src/getters.js +151 -139
  24. package/src/index.js +223 -170
  25. package/src/loggers.js +156 -139
  26. package/src/normalizers.js +28 -6
  27. package/src/options.js +133 -0
  28. package/src/parsers.js +197 -145
  29. package/src/paths.js +41 -0
  30. package/src/prompt.js +109 -0
  31. package/src/rebuild.js +106 -0
  32. package/src/rows.js +46 -0
  33. package/src/socketServer.js +311 -229
  34. package/src/templateDrift.js +93 -0
  35. package/src/versions.js +112 -0
  36. package/ui/README.md +55 -40
  37. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
  38. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
  39. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
  40. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
  41. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
  42. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
  43. package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
  44. package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
  45. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
  46. package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
  47. package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
  48. package/ui/dist/spa/index.html +2 -2
  49. package/ui/package.json +43 -42
  50. package/ui/src/components/DefinitionDiff.vue +169 -0
  51. package/ui/src/pages/MainComponent.vue +704 -78
  52. package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
  53. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
  54. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
  55. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
  56. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
  57. package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
  58. package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
  59. package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
  60. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  61. package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
  62. 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
- export const applyDifferences = async (connection, database, differences) => {
4
-
5
- differences.sort((a, b) => {
6
- //Sort to make sure we apply the differences in a logical order.
7
- const typePriority = {
8
- 'missing_database': 1,
9
- 'missing_table': 2,
10
- 'extra_table': 2,
11
- 'missing_field': 3,
12
- 'extra_field': 3,
13
- 'mismatched_field': 3,
14
- 'missing_index': 4,
15
- 'extra_index': 4,
16
- 'mismatched_index': 4,
17
- 'missing_procedure': 5,
18
- 'mismatched_procedure': 5,
19
- 'missing_view': 6,
20
- 'mismatched_view': 6,
21
- 'extra_view': 6,
22
- 'missing_trigger': 7,
23
- 'mismatched_trigger': 7,
24
- 'extra_trigger': 7
25
- };
26
-
27
- return (typePriority[a.type] || 999) - (typePriority[b.type] || 999);
28
- })
29
-
30
-
31
- for (const diff of differences) {
32
- try {
33
-
34
- if(diff.type == 'missing_database'){
35
- const createDatabaseSQL = `CREATE DATABASE \`${differences[0].database}\`;`;
36
- console.log(`Executing: ${createDatabaseSQL}`);
37
- await connection.query(createDatabaseSQL);
38
- continue;
39
- }
40
-
41
-
42
- await connection.query(`USE \`${database}\``);
43
- if (diff.type === 'missing_table') {
44
- const { tableName, createSQL } = diff;
45
- logger(`Creating missing table ${tableName}`);
46
- console.log(`Executing: ${createSQL}`);
47
- await connection.query(createSQL);
48
-
49
- } else if (diff.type === 'extra_table') {
50
- const { tableName } = diff;
51
- logger(`Dropping extra table ${tableName}`);
52
- const dropTableSQL = `DROP TABLE \`${tableName}\`;`;
53
- console.log(`Executing: ${dropTableSQL}`);
54
- await connection.query(dropTableSQL);
55
-
56
- } else if (diff.type === 'missing_field') {
57
- // Step 1: Add column with DEFAULT NULL to handle existing records
58
- const addFieldSQL = `ALTER TABLE \`${diff.tableName}\` ADD \`${diff.field.Field}\` ${diff.field.Type} DEFAULT NULL;`;
59
- console.log(`Executing: ${addFieldSQL}`);
60
- await connection.query(addFieldSQL);
61
-
62
- // Step 2: If NOT NULL is required and no default was specified, modify the column to remove DEFAULT NULL
63
- if (diff.field.NotNull && !diff.field.Default) {
64
- const modifyFieldSQL = `ALTER TABLE \`${diff.tableName}\` MODIFY \`${diff.field.Field}\` ${diff.field.Type} NOT NULL;`;
65
- console.log(`Executing: ${modifyFieldSQL}`);
66
- await connection.query(modifyFieldSQL);
67
- }
68
- if(diff.field.Default){
69
- const defaultClause = diff.field.Default === 'CURRENT_TIMESTAMP'
70
- ? 'DEFAULT CURRENT_TIMESTAMP'
71
- : diff.field.Default !== undefined && diff.field.Default !== null
72
- ? `DEFAULT '${diff.field.Default}'`
73
- : '';
74
- const modifyFieldSQL = `ALTER TABLE \`${diff.tableName}\` MODIFY \`${diff.field.Field}\` ${diff.field.Type} ${defaultClause};`;
75
- console.log(`Executing: ${modifyFieldSQL}`);
76
- await connection.query(modifyFieldSQL);
77
- }
78
-
79
- } else if (diff.type === 'extra_field') {
80
- const dropFieldSQL = `ALTER TABLE \`${diff.tableName}\` DROP COLUMN \`${diff.field.Field}\`;`;
81
- console.log(`Executing: ${dropFieldSQL}`);
82
- await connection.query(dropFieldSQL);
83
-
84
- } else if (diff.type === 'mismatched_field') {
85
- const { tableName, field, currentField } = diff;
86
-
87
- // use alter table rather than adding fields
88
-
89
- // // Helper function to determine if we need batch processing
90
- // const needsBatchProcessing = () => {
91
- // const currentType = currentField.Type.toLowerCase();
92
- // const newType = field.Type.toLowerCase();
93
-
94
- // // Decimal changes
95
- // if (currentType.includes('decimal') && newType.includes('decimal')) {
96
- // return true;
97
- // }
98
-
99
- // // VARCHAR/CHAR length reductions
100
- // if ((currentType.includes('varchar') || currentType.includes('char')) &&
101
- // (newType.includes('varchar') || newType.includes('char'))) {
102
- // const currentLength = parseInt(currentType.match(/\((\d+)\)/)?.[1] || '0');
103
- // const newLength = parseInt(newType.match(/\((\d+)\)/)?.[1] || '0');
104
- // return newLength < currentLength;
105
- // }
106
-
107
- // // TEXT to VARCHAR conversions
108
- // if (currentType.includes('text') && newType.includes('varchar')) {
109
- // return true;
110
- // }
111
-
112
- // // Numeric type downgrades
113
- // const numericDowngrades = {
114
- // 'bigint': ['int', 'mediumint', 'smallint', 'tinyint'],
115
- // 'int': ['mediumint', 'smallint', 'tinyint'],
116
- // 'mediumint': ['smallint', 'tinyint'],
117
- // 'smallint': ['tinyint'],
118
- // 'double': ['float', 'decimal'],
119
- // 'float': ['decimal']
120
- // };
121
-
122
- // for (const [larger, smaller] of Object.entries(numericDowngrades)) {
123
- // if (currentType.includes(larger) && smaller.some(t => newType.includes(t))) {
124
- // return true;
125
- // }
126
- // }
127
-
128
- // return false;
129
- // };
130
-
131
- // if (needsBatchProcessing()) {
132
- // // Step 1: Add new column
133
- // const addColumnSQL = `ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.Field}_new\` ${field.Type} DEFAULT NULL;`;
134
- // console.log(`Adding new column for type conversion: ${addColumnSQL}`);
135
- // await connection.query(addColumnSQL);
136
-
137
- // // Step 2: Copy data in batches
138
- // let totalUpdated = 0;
139
- // let batchSize = 10000;
140
- // let batchCount = 0;
141
-
142
- // while (true) {
143
- // const updateSQL = `
144
- // UPDATE \`${tableName}\`
145
- // SET \`${field.Field}_new\` = \`${field.Field}\`
146
- // WHERE \`${field.Field}_new\` IS NULL
147
- // AND \`${field.Field}\` IS NOT NULL
148
- // LIMIT ${batchSize};
149
- // `;
150
- // const result = await connection.query(updateSQL);
151
- // const rowsAffected = result.affectedRows;
152
-
153
- // if (rowsAffected === 0) break;
154
-
155
- // totalUpdated += rowsAffected;
156
- // batchCount++;
157
- // console.log(`Batch ${batchCount}: Converted ${rowsAffected} rows. Total converted: ${totalUpdated}`);
158
- // }
159
-
160
- // // Step 3: Verify data (optional)
161
- // const verifySQL = `
162
- // SELECT COUNT(*) as mismatch FROM \`${tableName}\`
163
- // WHERE \`${field.Field}\` IS NOT NULL
164
- // AND \`${field.Field}_new\` IS NULL;
165
- // `;
166
- // const [verifyResult] = await connection.query(verifySQL);
167
- // if (verifyResult.mismatch > 0) {
168
- // console.log(`Warning: ${verifyResult.mismatch} rows may have failed conversion`);
169
- // }
170
-
171
- // // Step 4: Swap columns
172
- // const swapColumnsSQL = `
173
- // ALTER TABLE \`${tableName}\`
174
- // DROP COLUMN \`${field.Field}\`,
175
- // RENAME COLUMN \`${field.Field}_new\` TO \`${field.Field}\`
176
- // `;
177
- // console.log(`Executing: ${swapColumnsSQL}`);
178
- // await connection.query(swapColumnsSQL);
179
-
180
- // console.log(`Completed type conversion for ${field.Field}. Total rows converted: ${totalUpdated}`);
181
- // } else {
182
- // Original mismatched field handling for simple changes
183
- const isTimestamp = field.Type.toLowerCase().includes('timestamp');
184
-
185
- // Handle timestamp specific attributes
186
- const defaultClause = field.Default === 'CURRENT_TIMESTAMP'
187
- ? 'DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
188
- : field.Default === 'NULL'
189
- ? isTimestamp ? 'NULL DEFAULT NULL' : 'DEFAULT NULL'
190
- : field.Default !== undefined && field.Default !== null
191
- ? `DEFAULT '${field.Default}'`
192
- : isTimestamp ? 'NULL DEFAULT NULL' : 'DEFAULT NULL';
193
-
194
- // Remove ON UPDATE CURRENT_TIMESTAMP if we're changing away from CURRENT_TIMESTAMP
195
- const hasOnUpdate = currentField?.Default === 'CURRENT_TIMESTAMP' &&
196
- field.Default !== 'CURRENT_TIMESTAMP';
197
-
198
- const modifyFieldSQL = `ALTER TABLE \`${tableName}\` CHANGE \`${field.Field}\` \`${field.Field}\` ${field.Type} ${field.NotNull ? 'NOT NULL' : ''} ${defaultClause}${hasOnUpdate ? ' ON UPDATE CURRENT_TIMESTAMP' : ''};`;
199
- console.log(`Executing: ${modifyFieldSQL}`);
200
- await connection.query(modifyFieldSQL);
201
- // }
202
-
203
- } else if (diff.type === 'missing_index') {
204
- const existingColumns = await connection.query(`SHOW COLUMNS FROM \`${diff.tableName}\``);
205
- const columnNames = existingColumns.map(col => col.Field);
206
-
207
- // Check if all columns for the index exist
208
- const missingColumns = Array.isArray(diff.index.ColumnName) ? diff.index.ColumnName.filter(col => !columnNames.includes(col)) : [];
209
-
210
- if (missingColumns.length > 0) {
211
- logger(`Cannot create index ${diff.index.Name} on ${diff.tableName}. Missing columns: ${missingColumns.join(', ')}`);
212
- continue; // Skip index creation if columns are missing
213
- }
214
-
215
- // Prepare the index columns with proper formatting
216
- const indexColumns = diff.index.ColumnName.map(col => `\`${col}\``); // Format column names
217
-
218
- // Handle PRIMARY index creation
219
- if (diff.index.Name === 'PRIMARY') {
220
- if (indexColumns.length > 0) {
221
- const primaryKeySQL = `ALTER TABLE \`${diff.tableName}\` ADD PRIMARY KEY (${indexColumns.join(', ')});`;
222
- console.log(`Executing: ${primaryKeySQL}`);
223
- await connection.query(primaryKeySQL);
224
- } else {
225
- logger(`Cannot create PRIMARY index on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
226
- }
227
- } else {
228
- // Create the new index with UNIQUE if applicable
229
- const uniqueSQL = diff.index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
230
- if (indexColumns.length > 0) {
231
- const addIndexSQL = `ALTER TABLE \`${diff.tableName}\` ADD ${uniqueSQL}INDEX \`${diff.index.Name}\` (${indexColumns.join(', ')});`;
232
- console.log(`Executing: ${addIndexSQL}`);
233
- await connection.query(addIndexSQL);
234
- } else {
235
- logger(`Cannot create index ${diff.index.Name} on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
236
- }
237
- }
238
-
239
- } else if (diff.type === 'extra_index') {
240
- // Handle dropping of extra indexes
241
- if (diff.index.Name === 'PRIMARY') {
242
- const dropPrimaryKeySQL = `ALTER TABLE \`${diff.tableName}\` DROP PRIMARY KEY;`;
243
- console.log(`Executing: ${dropPrimaryKeySQL}`);
244
- await connection.query(dropPrimaryKeySQL);
245
- } else {
246
- const dropIndexSQL = `DROP INDEX \`${diff.index.Name}\` ON \`${diff.tableName}\`;`;
247
- console.log(`Executing: ${dropIndexSQL}`);
248
- await connection.query(dropIndexSQL);
249
- }
250
-
251
- } else if (diff.type === 'mismatched_index') {
252
- const { tableName, index } = diff;
253
- // Drop the existing index
254
- if (index.Name === 'PRIMARY') {
255
- const dropPrimaryKeySQL = `ALTER TABLE \`${tableName}\` DROP PRIMARY KEY;`;
256
- console.log(`Executing: ${dropPrimaryKeySQL}`);
257
- await connection.query(dropPrimaryKeySQL);
258
- } else {
259
- const dropIndexSQL = `DROP INDEX \`${index.Name}\` ON \`${tableName}\`;`;
260
- console.log(`Executing: ${dropIndexSQL}`);
261
- await connection.query(dropIndexSQL);
262
- }
263
- console.log('index', index)
264
-
265
- // Create the new index
266
- const uniqueSQL = index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
267
- if (index.Name === 'PRIMARY') {
268
- const addPrimaryKeySQL = `ALTER TABLE \`${tableName}\` ADD PRIMARY KEY (${index.ColumnName.join(', ')});`;
269
- console.log(`Executing: ${addPrimaryKeySQL}`);
270
- await connection.query(addPrimaryKeySQL);
271
- } else {
272
- const addIndexSQL = `ALTER TABLE \`${tableName}\` ADD ${uniqueSQL}INDEX \`${index.Name}\` (${index.ColumnName.join(', ')});`; // Include UNIQUE if applicable
273
- console.log(`Executing: ${addIndexSQL}`);
274
- await connection.query(addIndexSQL);
275
- }
276
-
277
- } else if (diff.type === 'missing_procedure' || diff.type === 'mismatched_procedure') {
278
-
279
- // Drop the procedure if it exists
280
- const dropCurrentProcSQL = `DROP PROCEDURE IF EXISTS \`${diff.Name}\`;`;
281
- console.log(`Executing: ${dropCurrentProcSQL}`);
282
- await connection.query(dropCurrentProcSQL);
283
-
284
- // Create the procedure
285
- const createProcSQL = diff.Definition;
286
- console.log(`Executing: ${createProcSQL}`); // Log the SQL statement
287
- await connection.query(createProcSQL);
288
-
289
-
290
-
291
- } else if (diff.type === 'missing_view') {
292
- const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
293
- console.log(`Executing: ${dropViewSQL}`);
294
- await connection.query(dropViewSQL);
295
- const createViewSQL = diff.definition; // Use the expected definition to create the view
296
- console.log(`Executing: ${createViewSQL}`);
297
- await connection.query(createViewSQL);
298
-
299
-
300
- } else if (diff.type === 'mismatched_view') {
301
- const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
302
- console.log(`Executing: ${dropViewSQL}`);
303
- await connection.query(dropViewSQL);
304
- const createViewSQL = diff.definition; // Use the expected definition to create the view
305
- console.log(`Executing: ${createViewSQL}`);
306
- await connection.query(createViewSQL);
307
-
308
-
309
- } else if (diff.type === 'extra_view') {
310
- const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
311
- console.log(`Executing: ${dropViewSQL}`);
312
- await connection.query(dropViewSQL);
313
-
314
-
315
- } else if (diff.type === 'missing_trigger') {
316
- const createTriggerSQL = diff.trigger.Definition;
317
- logger('fixing trigger', diff.trigger.Name)
318
- console.log(`Executing: ${createTriggerSQL}`);
319
- await connection.query(createTriggerSQL);
320
-
321
-
322
- } else if (diff.type === 'mismatched_trigger') {
323
- const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
324
- console.log(`Executing: ${dropTriggerSQL}`);
325
- await connection.query(dropTriggerSQL);
326
- const createTriggerSQL = diff.trigger.Definition;
327
- console.log(`Executing: ${createTriggerSQL}`);
328
- await connection.query(createTriggerSQL);
329
-
330
-
331
- } else if (diff.type === 'extra_trigger') {
332
- const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
333
- console.log(`Executing: ${dropTriggerSQL}`);
334
- await connection.query(dropTriggerSQL);
335
-
336
- }else if(diff.type === 'missing_database'){
337
- logger('fixing missing database', diff.database)
338
- const createDatabaseSQL = `CREATE DATABASE \`${diff.database}\`;`;
339
- console.log(`Executing: ${createDatabaseSQL}`);
340
- await connection.query(createDatabaseSQL);
341
- } else {
342
- logger(`Error applying difference of type:${diff.type}`, diff)
343
- }
344
- } catch (e) {
345
- logger(`Error applying difference of type:${diff.type}. Error: ${e.message}`, diff);
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('; ')}`;