mysqlsyncr 1.1.12 → 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 +59 -0
- 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 +4059 -1733
- package/dist/index.js +4101 -1777
- package/package.json +66 -59
- package/rollup.config.js +2 -1
- package/src/DBStructureChecker.js +47 -0
- package/src/acceptCurrent.js +225 -0
- package/src/apply.js +254 -136
- 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 -169
- package/src/loggers.js +156 -139
- package/src/normalizers.js +28 -6
- package/src/options.js +133 -0
- package/src/parsers.js +82 -36
- 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 +106 -24
- 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,6 +1,167 @@
|
|
|
1
1
|
import { logger } from './loggers.js';
|
|
2
|
+
import { adaptSQLForServer, getServerVersion, portableCollation } from './versions.js';
|
|
2
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
|
+
*/
|
|
3
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
|
+
}
|
|
4
165
|
|
|
5
166
|
differences.sort((a, b) => {
|
|
6
167
|
//Sort to make sure we apply the differences in a logical order.
|
|
@@ -21,7 +182,9 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
21
182
|
'extra_view': 6,
|
|
22
183
|
'missing_trigger': 7,
|
|
23
184
|
'mismatched_trigger': 7,
|
|
24
|
-
'extra_trigger': 7
|
|
185
|
+
'extra_trigger': 7,
|
|
186
|
+
// after the table and its columns exist
|
|
187
|
+
'missing_row': 8
|
|
25
188
|
};
|
|
26
189
|
|
|
27
190
|
return (typePriority[a.type] || 999) - (typePriority[b.type] || 999);
|
|
@@ -29,57 +192,71 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
29
192
|
|
|
30
193
|
|
|
31
194
|
for (const diff of differences) {
|
|
195
|
+
current = diff;
|
|
32
196
|
try {
|
|
33
197
|
|
|
34
198
|
if(diff.type == 'missing_database'){
|
|
35
|
-
const createDatabaseSQL = `CREATE DATABASE \`${
|
|
199
|
+
const createDatabaseSQL = `CREATE DATABASE \`${diff.database}\`;`;
|
|
36
200
|
console.log(`Executing: ${createDatabaseSQL}`);
|
|
37
|
-
await
|
|
201
|
+
await query(createDatabaseSQL);
|
|
38
202
|
continue;
|
|
39
203
|
}
|
|
40
204
|
|
|
41
205
|
|
|
42
|
-
await
|
|
206
|
+
await query(`USE \`${database}\``);
|
|
43
207
|
if (diff.type === 'missing_table') {
|
|
44
208
|
const { tableName, createSQL } = diff;
|
|
45
209
|
logger(`Creating missing table ${tableName}`);
|
|
46
210
|
console.log(`Executing: ${createSQL}`);
|
|
47
|
-
|
|
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
|
+
}
|
|
48
242
|
|
|
49
243
|
} else if (diff.type === 'extra_table') {
|
|
50
244
|
const { tableName } = diff;
|
|
51
245
|
logger(`Dropping extra table ${tableName}`);
|
|
52
246
|
const dropTableSQL = `DROP TABLE \`${tableName}\`;`;
|
|
53
247
|
console.log(`Executing: ${dropTableSQL}`);
|
|
54
|
-
await
|
|
248
|
+
await query(dropTableSQL);
|
|
55
249
|
|
|
56
250
|
} else if (diff.type === 'missing_field') {
|
|
57
|
-
//
|
|
58
|
-
const addFieldSQL = `ALTER TABLE \`${diff.tableName}\` ADD \`${diff.field.Field}\` ${diff.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)};`;
|
|
59
253
|
console.log(`Executing: ${addFieldSQL}`);
|
|
60
|
-
await
|
|
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
|
-
}
|
|
254
|
+
await query(addFieldSQL);
|
|
78
255
|
|
|
79
256
|
} else if (diff.type === 'extra_field') {
|
|
80
257
|
const dropFieldSQL = `ALTER TABLE \`${diff.tableName}\` DROP COLUMN \`${diff.field.Field}\`;`;
|
|
81
258
|
console.log(`Executing: ${dropFieldSQL}`);
|
|
82
|
-
await
|
|
259
|
+
await query(dropFieldSQL);
|
|
83
260
|
|
|
84
261
|
} else if (diff.type === 'mismatched_field') {
|
|
85
262
|
const { tableName, field, currentField } = diff;
|
|
@@ -132,7 +309,7 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
132
309
|
// // Step 1: Add new column
|
|
133
310
|
// const addColumnSQL = `ALTER TABLE \`${tableName}\` ADD COLUMN \`${field.Field}_new\` ${field.Type} DEFAULT NULL;`;
|
|
134
311
|
// console.log(`Adding new column for type conversion: ${addColumnSQL}`);
|
|
135
|
-
// await
|
|
312
|
+
// await query(addColumnSQL);
|
|
136
313
|
|
|
137
314
|
// // Step 2: Copy data in batches
|
|
138
315
|
// let totalUpdated = 0;
|
|
@@ -147,7 +324,7 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
147
324
|
// AND \`${field.Field}\` IS NOT NULL
|
|
148
325
|
// LIMIT ${batchSize};
|
|
149
326
|
// `;
|
|
150
|
-
// const result = await
|
|
327
|
+
// const result = await query(updateSQL);
|
|
151
328
|
// const rowsAffected = result.affectedRows;
|
|
152
329
|
|
|
153
330
|
// if (rowsAffected === 0) break;
|
|
@@ -163,7 +340,7 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
163
340
|
// WHERE \`${field.Field}\` IS NOT NULL
|
|
164
341
|
// AND \`${field.Field}_new\` IS NULL;
|
|
165
342
|
// `;
|
|
166
|
-
// const [verifyResult] = await
|
|
343
|
+
// const [verifyResult] = await query(verifySQL);
|
|
167
344
|
// if (verifyResult.mismatch > 0) {
|
|
168
345
|
// console.log(`Warning: ${verifyResult.mismatch} rows may have failed conversion`);
|
|
169
346
|
// }
|
|
@@ -175,189 +352,130 @@ export const applyDifferences = async (connection, database, differences) => {
|
|
|
175
352
|
// RENAME COLUMN \`${field.Field}_new\` TO \`${field.Field}\`
|
|
176
353
|
// `;
|
|
177
354
|
// console.log(`Executing: ${swapColumnsSQL}`);
|
|
178
|
-
// await
|
|
355
|
+
// await query(swapColumnsSQL);
|
|
179
356
|
|
|
180
357
|
// console.log(`Completed type conversion for ${field.Field}. Total rows converted: ${totalUpdated}`);
|
|
181
358
|
// } else {
|
|
182
359
|
// Original mismatched field handling for simple changes
|
|
183
|
-
const
|
|
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' : ''};`;
|
|
360
|
+
const modifyFieldSQL = `ALTER TABLE \`${tableName}\` CHANGE \`${field.Field}\` \`${field.Field}\` ${columnDefinition(field, currentField)};`;
|
|
199
361
|
console.log(`Executing: ${modifyFieldSQL}`);
|
|
200
|
-
await
|
|
362
|
+
await query(modifyFieldSQL);
|
|
201
363
|
// }
|
|
202
364
|
|
|
203
365
|
} else if (diff.type === 'missing_index') {
|
|
204
|
-
const existingColumns = await
|
|
366
|
+
const existingColumns = await query(`SHOW COLUMNS FROM \`${diff.tableName}\``);
|
|
205
367
|
const columnNames = existingColumns.map(col => col.Field);
|
|
206
368
|
|
|
207
369
|
// Check if all columns for the index exist
|
|
208
370
|
const missingColumns = Array.isArray(diff.index.ColumnName) ? diff.index.ColumnName.filter(col => !columnNames.includes(col) && !columnNames.includes(col.split("(")?.[0])) : [];
|
|
209
371
|
|
|
210
372
|
if (missingColumns.length > 0) {
|
|
211
|
-
|
|
212
|
-
continue; // Skip index creation if columns are missing
|
|
373
|
+
throw new Error(`Cannot create index ${diff.index.Name} on ${diff.tableName}. Missing columns: ${missingColumns.join(', ')}`);
|
|
213
374
|
}
|
|
214
375
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if(col.includes("(")){
|
|
218
|
-
|
|
219
|
-
const match = col.match(/^(?:`([^`]+)`|([A-Za-z_][A-Za-z0-9_\$]*))\((\d+)\)$/);
|
|
220
|
-
|
|
221
|
-
if (match) {
|
|
222
|
-
const name = match[1] || match[2]; // one of them will be filled
|
|
223
|
-
const size = match[3];
|
|
224
|
-
console.log(name, size);
|
|
225
|
-
return `\`${name}\`(${size})`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return `\`${col.split("(")[0]}\``
|
|
229
|
-
} else {
|
|
230
|
-
return `\`${col}\``; // Format column names
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
// Handle PRIMARY index creation
|
|
234
|
-
if (diff.index.Name === 'PRIMARY') {
|
|
235
|
-
if (indexColumns.length > 0) {
|
|
236
|
-
const primaryKeySQL = `ALTER TABLE \`${diff.tableName}\` ADD PRIMARY KEY (${indexColumns.join(', ')});`;
|
|
237
|
-
console.log(`Executing: ${primaryKeySQL}`);
|
|
238
|
-
await connection.query(primaryKeySQL);
|
|
239
|
-
} else {
|
|
240
|
-
logger(`Cannot create PRIMARY index on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
// Create the new index with UNIQUE if applicable
|
|
244
|
-
const uniqueSQL = diff.index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
|
|
245
|
-
if (indexColumns.length > 0) {
|
|
246
|
-
const addIndexSQL = `ALTER TABLE \`${diff.tableName}\` ADD ${uniqueSQL}INDEX \`${diff.index.Name}\` (${indexColumns.join(', ')});`;
|
|
247
|
-
console.log(`Executing: ${addIndexSQL}`);
|
|
248
|
-
await connection.query(addIndexSQL);
|
|
249
|
-
} else {
|
|
250
|
-
logger(`Cannot create index ${diff.index.Name} on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
|
|
251
|
-
}
|
|
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.`);
|
|
252
378
|
}
|
|
379
|
+
const addIndexSQL = `ALTER TABLE \`${diff.tableName}\` ${addIndexClause(diff.index)};`;
|
|
380
|
+
console.log(`Executing: ${addIndexSQL}`);
|
|
381
|
+
await query(addIndexSQL);
|
|
253
382
|
|
|
254
383
|
} else if (diff.type === 'extra_index') {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
console.log(`Executing: ${dropPrimaryKeySQL}`);
|
|
259
|
-
await connection.query(dropPrimaryKeySQL);
|
|
260
|
-
} else {
|
|
261
|
-
const dropIndexSQL = `DROP INDEX \`${diff.index.Name}\` ON \`${diff.tableName}\`;`;
|
|
262
|
-
console.log(`Executing: ${dropIndexSQL}`);
|
|
263
|
-
await connection.query(dropIndexSQL);
|
|
264
|
-
}
|
|
384
|
+
const dropIndexSQL = `ALTER TABLE \`${diff.tableName}\` ${dropIndexClause(diff.index)};`;
|
|
385
|
+
console.log(`Executing: ${dropIndexSQL}`);
|
|
386
|
+
await query(dropIndexSQL);
|
|
265
387
|
|
|
266
388
|
} else if (diff.type === 'mismatched_index') {
|
|
267
389
|
const { tableName, index } = diff;
|
|
268
|
-
// Drop the
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
} else {
|
|
274
|
-
const dropIndexSQL = `DROP INDEX \`${index.Name}\` ON \`${tableName}\`;`;
|
|
275
|
-
console.log(`Executing: ${dropIndexSQL}`);
|
|
276
|
-
await connection.query(dropIndexSQL);
|
|
277
|
-
}
|
|
278
|
-
console.log('index', index)
|
|
279
|
-
|
|
280
|
-
// Create the new index
|
|
281
|
-
const uniqueSQL = index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
|
|
282
|
-
if (index.Name === 'PRIMARY') {
|
|
283
|
-
const addPrimaryKeySQL = `ALTER TABLE \`${tableName}\` ADD PRIMARY KEY (${index.ColumnName.join(', ')});`;
|
|
284
|
-
console.log(`Executing: ${addPrimaryKeySQL}`);
|
|
285
|
-
await connection.query(addPrimaryKeySQL);
|
|
286
|
-
} else {
|
|
287
|
-
const addIndexSQL = `ALTER TABLE \`${tableName}\` ADD ${uniqueSQL}INDEX \`${index.Name}\` (${index.ColumnName.join(', ')});`; // Include UNIQUE if applicable
|
|
288
|
-
console.log(`Executing: ${addIndexSQL}`);
|
|
289
|
-
await connection.query(addIndexSQL);
|
|
290
|
-
}
|
|
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);
|
|
291
395
|
|
|
292
396
|
} else if (diff.type === 'missing_procedure' || diff.type === 'mismatched_procedure') {
|
|
293
397
|
|
|
294
398
|
// Drop the procedure if it exists
|
|
295
399
|
const dropCurrentProcSQL = `DROP PROCEDURE IF EXISTS \`${diff.Name}\`;`;
|
|
296
400
|
console.log(`Executing: ${dropCurrentProcSQL}`);
|
|
297
|
-
await
|
|
401
|
+
await query(dropCurrentProcSQL);
|
|
298
402
|
|
|
299
403
|
// Create the procedure
|
|
300
404
|
const createProcSQL = diff.Definition;
|
|
301
405
|
console.log(`Executing: ${createProcSQL}`); // Log the SQL statement
|
|
302
|
-
await
|
|
406
|
+
await query(createProcSQL);
|
|
303
407
|
|
|
304
408
|
|
|
305
409
|
|
|
306
410
|
} else if (diff.type === 'missing_view') {
|
|
307
411
|
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
308
412
|
console.log(`Executing: ${dropViewSQL}`);
|
|
309
|
-
await
|
|
413
|
+
await query(dropViewSQL);
|
|
310
414
|
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
311
415
|
console.log(`Executing: ${createViewSQL}`);
|
|
312
|
-
await
|
|
416
|
+
await query(createViewSQL);
|
|
313
417
|
|
|
314
418
|
|
|
315
419
|
} else if (diff.type === 'mismatched_view') {
|
|
316
420
|
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
317
421
|
console.log(`Executing: ${dropViewSQL}`);
|
|
318
|
-
await
|
|
422
|
+
await query(dropViewSQL);
|
|
319
423
|
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
320
424
|
console.log(`Executing: ${createViewSQL}`);
|
|
321
|
-
await
|
|
425
|
+
await query(createViewSQL);
|
|
322
426
|
|
|
323
427
|
|
|
324
428
|
} else if (diff.type === 'extra_view') {
|
|
325
429
|
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
326
430
|
console.log(`Executing: ${dropViewSQL}`);
|
|
327
|
-
await
|
|
431
|
+
await query(dropViewSQL);
|
|
328
432
|
|
|
329
433
|
|
|
330
434
|
} else if (diff.type === 'missing_trigger') {
|
|
331
435
|
const createTriggerSQL = diff.trigger.Definition;
|
|
332
436
|
logger('fixing trigger', diff.trigger.Name)
|
|
333
437
|
console.log(`Executing: ${createTriggerSQL}`);
|
|
334
|
-
await
|
|
438
|
+
await query(createTriggerSQL);
|
|
335
439
|
|
|
336
440
|
|
|
337
441
|
} else if (diff.type === 'mismatched_trigger') {
|
|
338
442
|
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
339
443
|
console.log(`Executing: ${dropTriggerSQL}`);
|
|
340
|
-
await
|
|
444
|
+
await query(dropTriggerSQL);
|
|
341
445
|
const createTriggerSQL = diff.trigger.Definition;
|
|
342
446
|
console.log(`Executing: ${createTriggerSQL}`);
|
|
343
|
-
await
|
|
447
|
+
await query(createTriggerSQL);
|
|
344
448
|
|
|
345
449
|
|
|
346
450
|
} else if (diff.type === 'extra_trigger') {
|
|
347
451
|
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
348
452
|
console.log(`Executing: ${dropTriggerSQL}`);
|
|
349
|
-
await
|
|
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]));
|
|
350
460
|
|
|
351
461
|
}else if(diff.type === 'missing_database'){
|
|
352
462
|
logger('fixing missing database', diff.database)
|
|
353
463
|
const createDatabaseSQL = `CREATE DATABASE \`${diff.database}\`;`;
|
|
354
464
|
console.log(`Executing: ${createDatabaseSQL}`);
|
|
355
|
-
await
|
|
465
|
+
await query(createDatabaseSQL);
|
|
356
466
|
} else {
|
|
357
|
-
|
|
467
|
+
throw new Error(`Unknown difference type: ${diff.type}`);
|
|
358
468
|
}
|
|
359
469
|
} catch (e) {
|
|
360
|
-
|
|
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 });
|
|
361
473
|
}
|
|
362
474
|
}
|
|
363
|
-
|
|
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('; ')}`;
|
package/src/cli.js
CHANGED
|
@@ -1,35 +1,62 @@
|
|
|
1
|
-
import yargs from 'yargs';
|
|
2
|
-
import { hideBin } from 'yargs/helpers';
|
|
3
|
-
import { setVerbose } from './loggers.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
.option('
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { setVerbose } from './loggers.js';
|
|
4
|
+
import { promptForOptions, promptForPassword, DATABASE_COMMANDS } from './prompt.js';
|
|
5
|
+
|
|
6
|
+
const COMMANDS = ['dump', 'compare', 'update', 'ui', 'rebuild'];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse the command line. With no command, asks for the command and its options
|
|
10
|
+
* in a terminal, or prints the help otherwise. command is null when there's nothing to run.
|
|
11
|
+
*/
|
|
12
|
+
export const cli = async () => {
|
|
13
|
+
const parser = yargs(hideBin(process.argv))
|
|
14
|
+
.command('dump', 'Dump the database structure to files')
|
|
15
|
+
.command('compare', 'Compare the database structure with JSON dumps (dry run)')
|
|
16
|
+
.command('update', 'Update the database structure based on JSON dumps')
|
|
17
|
+
.command('ui', 'Open the UI')
|
|
18
|
+
.command('rebuild', "Rebuild the dump's JSON from each table's CREATE TABLE and list what changes (dry run unless --write)")
|
|
19
|
+
.option('uiPort', { alias: 'U', describe: 'UI port', default: 3600 })
|
|
20
|
+
.option('openUI', { alias: 'O', describe: 'Open UI', default: true })
|
|
21
|
+
.option('host', { alias: 'h', describe: 'Database host', default: 'localhost' })
|
|
22
|
+
.option('port', { alias: 'P', describe: 'Database port', default: 3306 })
|
|
23
|
+
.option('user', { alias: 'u', describe: 'Database user', default: 'root' })
|
|
24
|
+
// No default, so a missing password can be asked for; -p with no value means none
|
|
25
|
+
.option('password', { alias: 'p', describe: 'Database password (asked for in a terminal when not given)', type: 'string' })
|
|
26
|
+
.option('database', { alias: 'd', describe: 'Target database', default: null })
|
|
27
|
+
.option('output', { alias: 'o', describe: 'Output directory for dumping. default is one level up from the current directory in a new folder called db-dump', default: '../db-dump' })
|
|
28
|
+
.option('write', { describe: 'With rebuild, write the rebuilt files', type: 'boolean', default: false })
|
|
29
|
+
.option('verbose', { alias: 'v', describe: 'Enable verbose logging', type: 'boolean', default: false })
|
|
30
|
+
// .demandCommand(1)
|
|
31
|
+
.help();
|
|
32
|
+
const argv = parser.argv;
|
|
33
|
+
|
|
34
|
+
let command = argv._[0];
|
|
35
|
+
if (command === undefined) {
|
|
36
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
37
|
+
command = await promptForOptions(argv);
|
|
38
|
+
} else {
|
|
39
|
+
parser.showHelp();
|
|
40
|
+
command = null;
|
|
41
|
+
}
|
|
42
|
+
} else if (!COMMANDS.includes(command)) {
|
|
43
|
+
console.error(`Unknown command: ${command}. Use one of: ${COMMANDS.join(', ')}`);
|
|
44
|
+
command = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
48
|
+
if (DATABASE_COMMANDS.includes(command) && argv.password === undefined) {
|
|
49
|
+
argv.password = interactive ? await promptForPassword(argv) : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = {
|
|
53
|
+
host: argv.host,
|
|
54
|
+
port: argv.port,
|
|
55
|
+
user: argv.user,
|
|
56
|
+
password: argv.password,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
setVerbose(argv.verbose);
|
|
60
|
+
|
|
61
|
+
return { command, config, argv };
|
|
35
62
|
};
|