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
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { parseCreateTableSQL, parseColumnDefinition } from '../src/parsers';
|
|
2
|
+
import { findDifferences } from '../src/differences';
|
|
3
|
+
import { applyDifferences, columnDefinition } from '../src/apply';
|
|
4
|
+
|
|
5
|
+
// A one-table structure the way getDatabaseStructure returns it
|
|
6
|
+
const liveStructure = (createSQL) => {
|
|
7
|
+
const parsed = parseCreateTableSQL(createSQL);
|
|
8
|
+
return {
|
|
9
|
+
tables: { [parsed.name]: { columns: parsed.columns, indexes: parsed.indexes, constraints: parsed.constraints, triggers: [], createSQL } },
|
|
10
|
+
procedures: []
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// A one-table template: hand-written JSON columns/indexes alongside its createSQL
|
|
15
|
+
const templateStructure = (createSQL, columns, indexes) => ({
|
|
16
|
+
tables: { [parseCreateTableSQL(createSQL).name]: { columns, indexes, triggers: [], createSQL } },
|
|
17
|
+
procedures: []
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Records every statement instead of running it; statements matching failOn throw
|
|
21
|
+
const mockConnection = (failOn = []) => {
|
|
22
|
+
const statements = [];
|
|
23
|
+
return {
|
|
24
|
+
statements,
|
|
25
|
+
query: async (sql) => {
|
|
26
|
+
if (sql.startsWith('USE') || sql.startsWith('SELECT VERSION') || sql.startsWith('SET SESSION')) return [];
|
|
27
|
+
if (sql.startsWith('SHOW COLUMNS')) return [{ Field: 'alarmString' }];
|
|
28
|
+
statements.push(sql);
|
|
29
|
+
const failure = failOn.find(f => sql.includes(f));
|
|
30
|
+
if (failure) throw new Error(`failed on ${failure}`);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('parseColumnDefinition', () => {
|
|
37
|
+
test('reads DEFAULT after COLLATE', () => {
|
|
38
|
+
expect(parseColumnDefinition('`IID` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL,')).toEqual({
|
|
39
|
+
Field: 'IID', Type: 'varchar(25)', Collate: 'utf8mb4_unicode_ci', Default: 'NULL'
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('reads DEFAULT after a two-parameter type', () => {
|
|
44
|
+
expect(parseColumnDefinition('`currentValue` float(10,2) DEFAULT NULL,')).toEqual({
|
|
45
|
+
Field: 'currentValue', Type: 'float(10,2)', Default: 'NULL'
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('keeps enum values in the type', () => {
|
|
50
|
+
expect(parseColumnDefinition("`changeType` enum('INSERT','UPDATE','DELETE') NOT NULL,")).toEqual({
|
|
51
|
+
Field: 'changeType', Type: "enum('INSERT','UPDATE','DELETE')", NotNull: true
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('keeps spaces and escaped quotes in a quoted default', () => {
|
|
56
|
+
expect(parseColumnDefinition("`modifiedBy` varchar(32) NOT NULL DEFAULT 'System Generated',").Default).toBe('System Generated');
|
|
57
|
+
expect(parseColumnDefinition("`note` varchar(32) DEFAULT 'it''s, ok',").Default).toBe("it's, ok");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('ignores keywords inside a comment', () => {
|
|
61
|
+
expect(parseColumnDefinition("`notes` text COMMENT 'NOT NULL DEFAULT x, ON UPDATE CURRENT_TIMESTAMP',")).toEqual({
|
|
62
|
+
Field: 'notes', Type: 'text', Comment: 'NOT NULL DEFAULT x, ON UPDATE CURRENT_TIMESTAMP'
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('reads ON UPDATE', () => {
|
|
67
|
+
expect(parseColumnDefinition('`dateUpdated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,')).toEqual({
|
|
68
|
+
Field: 'dateUpdated', Type: 'timestamp', NotNull: true, Default: 'CURRENT_TIMESTAMP', OnUpdate: 'CURRENT_TIMESTAMP'
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('returns null for index lines', () => {
|
|
73
|
+
expect(parseColumnDefinition('KEY `idx_IID` (`IID`,`point`),')).toBeNull();
|
|
74
|
+
expect(parseColumnDefinition('PRIMARY KEY (`alarmID`),')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('findDifferences on tables already matching the template', () => {
|
|
79
|
+
test('columns with COLLATE before DEFAULT', () => {
|
|
80
|
+
const createSQL = `CREATE TABLE \`E2Alarms\` (
|
|
81
|
+
\`alarmID\` bigint NOT NULL,
|
|
82
|
+
\`IID\` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
|
83
|
+
PRIMARY KEY (\`alarmID\`)
|
|
84
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`;
|
|
85
|
+
const template = templateStructure(createSQL + ';', [
|
|
86
|
+
{ Field: 'alarmID', Type: 'bigint', NotNull: true },
|
|
87
|
+
{ Field: 'IID', Type: 'varchar(25)', Default: 'NULL' }
|
|
88
|
+
], [{ ColumnName: ['alarmID'], Primary: true, Name: 'PRIMARY' }]);
|
|
89
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('enum type, quoted default with a space, and indexes with "Unique": false', () => {
|
|
93
|
+
const createSQL = `CREATE TABLE \`SystemCodesHistory\` (
|
|
94
|
+
\`idSystemCodesHistory\` int NOT NULL AUTO_INCREMENT,
|
|
95
|
+
\`assetID\` int NOT NULL,
|
|
96
|
+
\`changeType\` enum('INSERT','UPDATE','DELETE') NOT NULL,
|
|
97
|
+
\`modifiedBy\` varchar(32) NOT NULL DEFAULT 'System Generated',
|
|
98
|
+
PRIMARY KEY (\`idSystemCodesHistory\`),
|
|
99
|
+
KEY \`idx_systemcodeshistory_assetID\` (\`assetID\`)
|
|
100
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
101
|
+
const template = templateStructure(createSQL + ';', [
|
|
102
|
+
{ Field: 'idSystemCodesHistory', Type: 'int', NotNull: true, AutoIncrement: true },
|
|
103
|
+
{ Field: 'assetID', Type: 'int', NotNull: true },
|
|
104
|
+
{ Field: 'changeType', Type: "enum('INSERT','UPDATE','DELETE')", NotNull: true },
|
|
105
|
+
{ Field: 'modifiedBy', Type: 'varchar(32)', NotNull: true, Default: 'System Generated' }
|
|
106
|
+
], [
|
|
107
|
+
{ ColumnName: ['idSystemCodesHistory'], Unique: false, Name: 'PRIMARY' },
|
|
108
|
+
{ ColumnName: ['assetID'], Unique: false, Name: 'idx_systemcodeshistory_assetID' }
|
|
109
|
+
]);
|
|
110
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('nullable columns with and without an explicit DEFAULT NULL', () => {
|
|
114
|
+
// MySQL never prints DEFAULT NULL for TEXT columns
|
|
115
|
+
const createSQL = `CREATE TABLE \`data_alarms\` (
|
|
116
|
+
\`id\` int NOT NULL AUTO_INCREMENT,
|
|
117
|
+
\`confirm_notes\` text COMMENT 'optional notes',
|
|
118
|
+
\`notificationStartTime\` time DEFAULT NULL,
|
|
119
|
+
PRIMARY KEY (\`id\`)
|
|
120
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
121
|
+
const template = templateStructure(createSQL + ';', [
|
|
122
|
+
{ Field: 'id', Type: 'int', NotNull: true, AutoIncrement: true },
|
|
123
|
+
{ Field: 'confirm_notes', Type: 'text', Default: 'NULL' },
|
|
124
|
+
{ Field: 'notificationStartTime', Type: 'time', NotNull: false }
|
|
125
|
+
], [{ ColumnName: ['id'], Primary: true, Name: 'PRIMARY' }]);
|
|
126
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('prefix index recorded without its length in the JSON', () => {
|
|
130
|
+
const createSQL = `CREATE TABLE \`E2Alarms\` (
|
|
131
|
+
\`alarmID\` bigint NOT NULL,
|
|
132
|
+
\`alarmString\` mediumtext,
|
|
133
|
+
PRIMARY KEY (\`alarmID\`),
|
|
134
|
+
KEY \`idx_e2alarms_alarmstring\` (\`alarmString\`(255))
|
|
135
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
136
|
+
const template = templateStructure(createSQL + ';', [
|
|
137
|
+
{ Field: 'alarmID', Type: 'bigint', NotNull: true },
|
|
138
|
+
{ Field: 'alarmString', Type: 'mediumtext' }
|
|
139
|
+
], [
|
|
140
|
+
{ ColumnName: ['alarmID'], Primary: true, Name: 'PRIMARY' },
|
|
141
|
+
{ ColumnName: ['alarmString'], Name: 'idx_e2alarms_alarmstring' }
|
|
142
|
+
]);
|
|
143
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('findDifferences on tables that differ', () => {
|
|
148
|
+
test('a missing prefix index keeps its length from the createSQL', () => {
|
|
149
|
+
const templateSQL = `CREATE TABLE \`E2Alarms\` (
|
|
150
|
+
\`alarmID\` bigint NOT NULL,
|
|
151
|
+
\`alarmString\` mediumtext,
|
|
152
|
+
PRIMARY KEY (\`alarmID\`),
|
|
153
|
+
KEY \`idx_e2alarms_alarmstring\` (\`alarmString\`(255))
|
|
154
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1;`;
|
|
155
|
+
const liveSQL = `CREATE TABLE \`E2Alarms\` (
|
|
156
|
+
\`alarmID\` bigint NOT NULL,
|
|
157
|
+
\`alarmString\` mediumtext,
|
|
158
|
+
PRIMARY KEY (\`alarmID\`)
|
|
159
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
160
|
+
const template = templateStructure(templateSQL, [
|
|
161
|
+
{ Field: 'alarmID', Type: 'bigint', NotNull: true },
|
|
162
|
+
{ Field: 'alarmString', Type: 'mediumtext' }
|
|
163
|
+
], [
|
|
164
|
+
{ ColumnName: ['alarmID'], Primary: true, Name: 'PRIMARY' },
|
|
165
|
+
{ ColumnName: ['alarmString'], Name: 'idx_e2alarms_alarmstring' }
|
|
166
|
+
]);
|
|
167
|
+
const diffs = findDifferences(template, liveStructure(liveSQL));
|
|
168
|
+
expect(diffs).toHaveLength(1);
|
|
169
|
+
expect(diffs[0]).toMatchObject({ type: 'missing_index', index: { ColumnName: ['alarmString(255)'] } });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('a missing column picks up ON UPDATE from the createSQL', () => {
|
|
173
|
+
const templateSQL = `CREATE TABLE \`Alarms_Log\` (
|
|
174
|
+
\`idAlarmLog\` int NOT NULL AUTO_INCREMENT,
|
|
175
|
+
\`dateUpdated\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
176
|
+
PRIMARY KEY (\`idAlarmLog\`)
|
|
177
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
178
|
+
const liveSQL = `CREATE TABLE \`Alarms_Log\` (
|
|
179
|
+
\`idAlarmLog\` int NOT NULL AUTO_INCREMENT,
|
|
180
|
+
PRIMARY KEY (\`idAlarmLog\`)
|
|
181
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
182
|
+
const template = templateStructure(templateSQL, [
|
|
183
|
+
{ Field: 'idAlarmLog', Type: 'int', NotNull: true, AutoIncrement: true },
|
|
184
|
+
{ Field: 'dateUpdated', Type: 'timestamp', NotNull: true, Default: 'CURRENT_TIMESTAMP' }
|
|
185
|
+
], [{ ColumnName: ['idAlarmLog'], Primary: true, Name: 'PRIMARY' }]);
|
|
186
|
+
const diffs = findDifferences(template, liveStructure(liveSQL));
|
|
187
|
+
expect(diffs).toHaveLength(1);
|
|
188
|
+
expect(diffs[0]).toMatchObject({ type: 'missing_field', field: { Field: 'dateUpdated', OnUpdate: 'CURRENT_TIMESTAMP' } });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('detects a nullability change', () => {
|
|
192
|
+
const templateSQL = `CREATE TABLE \`t\` (
|
|
193
|
+
\`a\` int NOT NULL
|
|
194
|
+
) ENGINE=InnoDB;`;
|
|
195
|
+
const template = templateStructure(templateSQL, [{ Field: 'a', Type: 'int', NotNull: true }], []);
|
|
196
|
+
const notNullLive = liveStructure(templateSQL.replace(';', ' DEFAULT CHARSET=latin1'));
|
|
197
|
+
expect(findDifferences(template, notNullLive)).toEqual([]);
|
|
198
|
+
const nullableLive = liveStructure(templateSQL.replace('int NOT NULL', 'int DEFAULT NULL'));
|
|
199
|
+
expect(findDifferences(template, nullableLive).map(d => d.info)).toContain('Null');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('does not reorder composite index columns', () => {
|
|
203
|
+
const createSQL = `CREATE TABLE \`t\` (
|
|
204
|
+
\`b\` int NOT NULL,
|
|
205
|
+
\`a\` int NOT NULL,
|
|
206
|
+
KEY \`idx_ba\` (\`b\`,\`a\`)
|
|
207
|
+
) ENGINE=InnoDB`;
|
|
208
|
+
const template = templateStructure(createSQL, [
|
|
209
|
+
{ Field: 'b', Type: 'int', NotNull: true },
|
|
210
|
+
{ Field: 'a', Type: 'int', NotNull: true }
|
|
211
|
+
], [{ ColumnName: ['b', 'a'], Name: 'idx_ba' }]);
|
|
212
|
+
const live = liveStructure(createSQL.replace('`idx_ba`', '`other`'));
|
|
213
|
+
findDifferences(template, live);
|
|
214
|
+
expect(template.tables.t.indexes[0].ColumnName).toEqual(['b', 'a']);
|
|
215
|
+
expect(live.tables.t.indexes[0].ColumnName).toEqual(['b', 'a']);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('columnDefinition', () => {
|
|
220
|
+
test('NOT NULL without a default gets no DEFAULT NULL', () => {
|
|
221
|
+
expect(columnDefinition({ Field: 'DeviceId', Type: 'varchar(45)', NotNull: true })).toBe('varchar(45) NOT NULL');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('nullable timestamp is explicitly NULL', () => {
|
|
225
|
+
expect(columnDefinition({ Field: 'lastValueUpdated', Type: 'timestamp', Default: 'NULL' })).toBe('timestamp NULL DEFAULT NULL');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('CURRENT_TIMESTAMP default with ON UPDATE', () => {
|
|
229
|
+
expect(columnDefinition({ Field: 'dateUpdated', Type: 'timestamp', NotNull: true, Default: 'CURRENT_TIMESTAMP', OnUpdate: 'CURRENT_TIMESTAMP' }))
|
|
230
|
+
.toBe('timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('quotes string defaults', () => {
|
|
234
|
+
expect(columnDefinition({ Field: 'modifiedBy', Type: 'varchar(32)', NotNull: true, Default: "System's Generated" }))
|
|
235
|
+
.toBe("varchar(32) NOT NULL DEFAULT 'System''s Generated'");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('keeps the live collation and comment when the template has none recorded', () => {
|
|
239
|
+
expect(columnDefinition({ Field: 'IID', Type: 'varchar(25)', Default: 'NULL' }, { Field: 'IID', Type: 'varchar(25)', Collate: 'utf8mb4_unicode_ci', Comment: 'note' }))
|
|
240
|
+
.toBe("varchar(25) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'note'");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('a template without ON UPDATE removes it from the live column', () => {
|
|
244
|
+
expect(columnDefinition({ Field: 'd', Type: 'timestamp', Default: 'CURRENT_TIMESTAMP', OnUpdate: null }, { Field: 'd', Type: 'timestamp', OnUpdate: 'CURRENT_TIMESTAMP' }))
|
|
245
|
+
.toBe('timestamp NULL DEFAULT CURRENT_TIMESTAMP');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('applyDifferences', () => {
|
|
250
|
+
test('generates valid SQL for the store diffs', async () => {
|
|
251
|
+
const connection = mockConnection();
|
|
252
|
+
await applyDifferences(connection, 'Alerts', [
|
|
253
|
+
{ type: 'missing_field', tableName: 'Alarms_Log', field: { Field: 'dateUpdated', Type: 'timestamp', NotNull: true, Default: 'CURRENT_TIMESTAMP', OnUpdate: 'CURRENT_TIMESTAMP' } },
|
|
254
|
+
{ type: 'mismatched_field', info: 'Type', tableName: 'E2Data', field: { Field: 'DeviceId', Type: 'varchar(45)', NotNull: true }, currentField: { Field: 'DeviceId', Type: 'varchar(75)', NotNull: true } },
|
|
255
|
+
{ type: 'missing_index', tableName: 'E2Alarms', indexName: 'idx_e2alarms_alarmstring', index: { ColumnName: ['alarmString(255)'], Name: 'idx_e2alarms_alarmstring' } },
|
|
256
|
+
{ type: 'mismatched_index', from: 'primary', tableName: 'SystemCodesHistory', indexName: 'PRIMARY', index: { ColumnName: ['idSystemCodesHistory'], Name: 'PRIMARY' } }
|
|
257
|
+
]);
|
|
258
|
+
expect(connection.statements).toEqual([
|
|
259
|
+
'ALTER TABLE `Alarms_Log` ADD COLUMN `dateUpdated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;',
|
|
260
|
+
'ALTER TABLE `E2Data` CHANGE `DeviceId` `DeviceId` varchar(45) NOT NULL;',
|
|
261
|
+
'ALTER TABLE `E2Alarms` ADD INDEX `idx_e2alarms_alarmstring` (`alarmString`(255));',
|
|
262
|
+
'ALTER TABLE `SystemCodesHistory` DROP PRIMARY KEY, ADD PRIMARY KEY (`idSystemCodesHistory`);'
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('tries every difference, then returns the failures', async () => {
|
|
267
|
+
const connection = mockConnection(['`a`']);
|
|
268
|
+
const diffs = [
|
|
269
|
+
{ type: 'missing_field', tableName: 't', field: { Field: 'a', Type: 'int' } },
|
|
270
|
+
{ type: 'missing_field', tableName: 't', field: { Field: 'b', Type: 'int' } }
|
|
271
|
+
];
|
|
272
|
+
expect(await applyDifferences(connection, 'db', diffs)).toEqual({
|
|
273
|
+
attempted: 2,
|
|
274
|
+
failures: [{ type: 'missing_field', tableName: 't', description: 'missing_field t.a', message: 'failed on `a`' }],
|
|
275
|
+
warnings: []
|
|
276
|
+
});
|
|
277
|
+
expect(connection.statements).toHaveLength(2);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('mismatched procedures', () => {
|
|
282
|
+
test('include the database definition alongside the template one', () => {
|
|
283
|
+
const expected = { tables: {}, procedures: [{ Name: 'P', Definition: 'CREATE PROCEDURE `P`() SELECT 1' }] };
|
|
284
|
+
const current = { tables: {}, procedures: [{ Name: 'P', Definition: 'CREATE PROCEDURE `P`() SELECT 2' }] };
|
|
285
|
+
expect(findDifferences(expected, current)).toEqual([{
|
|
286
|
+
type: 'mismatched_procedure',
|
|
287
|
+
Name: 'P',
|
|
288
|
+
Definition: 'CREATE PROCEDURE `P`() SELECT 1',
|
|
289
|
+
currentDefinition: 'CREATE PROCEDURE `P`() SELECT 2'
|
|
290
|
+
}]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('templates written by older versions', () => {
|
|
295
|
+
const createSQL = `CREATE TABLE \`SystemCodesHistory\` (
|
|
296
|
+
\`id\` int NOT NULL AUTO_INCREMENT,
|
|
297
|
+
\`changeType\` enum('INSERT','UPDATE','DELETE') NOT NULL,
|
|
298
|
+
\`modifiedBy\` varchar(32) NOT NULL DEFAULT 'System Generated',
|
|
299
|
+
\`recallTime\` decimal(10,2) NOT NULL DEFAULT '60.00',
|
|
300
|
+
\`assetId\` int NOT NULL,
|
|
301
|
+
PRIMARY KEY (\`id\`),
|
|
302
|
+
CONSTRAINT \`fk_asset\` FOREIGN KEY (\`assetId\`) REFERENCES \`Alerts\`.\`Assets\` (\`idAsset\`)
|
|
303
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
304
|
+
// What the old parser stored for that createSQL
|
|
305
|
+
const legacyColumns = [
|
|
306
|
+
{ Field: 'id', Type: 'int', NotNull: true, AutoIncrement: true },
|
|
307
|
+
{ Field: 'changeType', Type: 'enum' },
|
|
308
|
+
{ Field: 'modifiedBy', Type: 'varchar(32)', NotNull: true, Default: 'System' },
|
|
309
|
+
{ Field: 'recallTime', Type: 'decimal(10,2)' },
|
|
310
|
+
{ Field: 'assetId', Type: 'int', NotNull: true },
|
|
311
|
+
{ Field: 'fk_asset', Type: 'FOREIGN' }
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
test('show no differences against a database that matches the createSQL', () => {
|
|
315
|
+
const template = templateStructure(createSQL + ';', legacyColumns, [{ ColumnName: ['id'], Primary: true, Name: 'PRIMARY' }]);
|
|
316
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('the createSQL is what counts, not edits made only to the JSON', () => {
|
|
320
|
+
const edited = legacyColumns.map(c => c.Field === 'modifiedBy' ? { ...c, Default: 'Someone Else' } : c);
|
|
321
|
+
const template = templateStructure(createSQL + ';', edited, [{ ColumnName: ['id'], Primary: true, Name: 'PRIMARY' }]);
|
|
322
|
+
expect(findDifferences(template, liveStructure(createSQL))).toEqual([]);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('columns the old parser lost attributes from', () => {
|
|
327
|
+
test('quoted parts of defaults and generated expressions are kept', () => {
|
|
328
|
+
expect(parseColumnDefinition("`flag` bit(1) NOT NULL DEFAULT b'0',").Default).toBe("b'0'");
|
|
329
|
+
expect(parseColumnDefinition("`tags` json DEFAULT (_utf8mb4'[]'),").Default).toBe("(_utf8mb4'[]')");
|
|
330
|
+
expect(parseColumnDefinition("`c` varchar(20) GENERATED ALWAYS AS (concat(_utf8mb4'a',`b`)) STORED,").Generated).toBe("(concat(_utf8mb4'a',`b`)) STORED");
|
|
331
|
+
expect(parseColumnDefinition("`n` int unsigned NOT NULL DEFAULT '0',").Type).toBe('int unsigned');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('column SQL keeps bit literals, unsigned and generated expressions', () => {
|
|
335
|
+
expect(columnDefinition({ Field: 'flag', Type: 'bit(1)', NotNull: true, Default: "b'0'" })).toBe("bit(1) NOT NULL DEFAULT b'0'");
|
|
336
|
+
expect(columnDefinition({ Field: 'n', Type: 'int unsigned', NotNull: true, Default: '0' })).toBe("int unsigned NOT NULL DEFAULT '0'");
|
|
337
|
+
expect(columnDefinition(
|
|
338
|
+
{ Field: 'c', Type: 'varchar(20)', Default: 'NULL' },
|
|
339
|
+
{ Field: 'c', Type: 'varchar(20)', Generated: "(concat(_utf8mb4'a',`b`)) STORED" }
|
|
340
|
+
)).toBe("varchar(20) GENERATED ALWAYS AS (concat(_utf8mb4'a',`b`)) STORED NULL");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('integer display widths from older MySQL versions still match, unsigned included', () => {
|
|
344
|
+
const template = templateStructure('CREATE TABLE `t` (\n `a` int unsigned NOT NULL\n);', [{ Field: 'a', Type: 'int unsigned', NotNull: true }], []);
|
|
345
|
+
expect(findDifferences(template, liveStructure('CREATE TABLE `t` (\n `a` int(10) unsigned NOT NULL\n)'))).toEqual([]);
|
|
346
|
+
expect(findDifferences(template, liveStructure('CREATE TABLE `t` (\n `a` int(10) NOT NULL\n)')).map(d => d.info)).toEqual(['Type']);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { splitSqlFile, findTemplateDrift, resolveTemplateDrift } from '../src/templateDrift';
|
|
5
|
+
import { parseCreateTableSQL } from '../src/parsers';
|
|
6
|
+
|
|
7
|
+
const jsonSQL = `CREATE TABLE \`CurrentValues\` (
|
|
8
|
+
\`mapplicID\` varchar(150) NOT NULL,
|
|
9
|
+
\`lastValueUpdated\` timestamp NULL DEFAULT NULL,
|
|
10
|
+
PRIMARY KEY (\`mapplicID\`)
|
|
11
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
12
|
+
const olderSQL = jsonSQL.replace(' `lastValueUpdated` timestamp NULL DEFAULT NULL,\n', '');
|
|
13
|
+
const notes = '\n-- Notes about these columns, written by hand\n-- that must survive either choice\n';
|
|
14
|
+
|
|
15
|
+
const setup = (sqlFile) => {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mysqlsyncr-drift-'));
|
|
17
|
+
fs.mkdirSync(path.join(dir, 'currentCOMPortData', 'tables'), { recursive: true });
|
|
18
|
+
const parsed = parseCreateTableSQL(jsonSQL);
|
|
19
|
+
fs.writeFileSync(path.join(dir, 'currentCOMPortData', 'currentCOMPortData.json'),
|
|
20
|
+
JSON.stringify({ tables: { CurrentValues: { columns: parsed.columns, indexes: parsed.indexes, constraints: [], triggers: [{ Name: 't' }], createSQL: jsonSQL } } }));
|
|
21
|
+
fs.writeFileSync(path.join(dir, 'currentCOMPortData', 'tables', 'CurrentValues.sql'), sqlFile);
|
|
22
|
+
return {
|
|
23
|
+
dir,
|
|
24
|
+
sqlFile: () => fs.readFileSync(path.join(dir, 'currentCOMPortData', 'tables', 'CurrentValues.sql'), 'utf-8'),
|
|
25
|
+
table: () => JSON.parse(fs.readFileSync(path.join(dir, 'currentCOMPortData', 'currentCOMPortData.json'), 'utf-8')).tables.CurrentValues
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('splitSqlFile', () => {
|
|
30
|
+
test('separates the statement from notes after it', () => {
|
|
31
|
+
expect(splitSqlFile(`${olderSQL};\n${notes}`)).toEqual({ before: '', statement: olderSQL, semicolon: true, after: notes });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('findTemplateDrift', () => {
|
|
36
|
+
test('notes after the statement are not a difference', async () => {
|
|
37
|
+
const { dir } = setup(`${jsonSQL};\n${notes}`);
|
|
38
|
+
expect(await findTemplateDrift(dir)).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a different statement is reported with both versions', async () => {
|
|
42
|
+
const { dir } = setup(`${olderSQL}\n`);
|
|
43
|
+
expect(await findTemplateDrift(dir)).toEqual([
|
|
44
|
+
{ database: 'currentCOMPortData', table: 'CurrentValues', file: 'tables/CurrentValues.sql', json: jsonSQL, sql: olderSQL }
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('resolveTemplateDrift', () => {
|
|
50
|
+
test('keeping the JSON rewrites the statement in the .sql file but keeps its notes', async () => {
|
|
51
|
+
const { dir, sqlFile } = setup(`${olderSQL};\n${notes}`);
|
|
52
|
+
await resolveTemplateDrift(dir, 'currentCOMPortData', 'CurrentValues', 'json');
|
|
53
|
+
expect(sqlFile()).toBe(`${jsonSQL};\n${notes}`);
|
|
54
|
+
expect(await findTemplateDrift(dir)).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('keeping the .sql file updates the JSON createSQL, columns and indexes', async () => {
|
|
58
|
+
const { dir, sqlFile, table } = setup(`${olderSQL};\n${notes}`);
|
|
59
|
+
await resolveTemplateDrift(dir, 'currentCOMPortData', 'CurrentValues', 'sql');
|
|
60
|
+
expect(table().createSQL).toBe(olderSQL);
|
|
61
|
+
expect(table().columns.map(c => c.Field)).toEqual(['mapplicID']);
|
|
62
|
+
expect(table().triggers).toEqual([{ Name: 't' }]);
|
|
63
|
+
expect(sqlFile()).toBe(`${olderSQL};\n${notes}`);
|
|
64
|
+
expect(await findTemplateDrift(dir)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('refuses an unknown choice or a file for another table', async () => {
|
|
68
|
+
const { dir } = setup(olderSQL.replace('`CurrentValues`', '`Other`'));
|
|
69
|
+
await expect(resolveTemplateDrift(dir, 'currentCOMPortData', 'CurrentValues', 'both')).rejects.toThrow("keep must be 'json' or 'sql'");
|
|
70
|
+
await expect(resolveTemplateDrift(dir, 'currentCOMPortData', 'CurrentValues', 'sql')).rejects.toThrow("doesn't hold a CREATE TABLE for CurrentValues");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { normalizeVersionFormatting } from '../src/normalizers';
|
|
5
|
+
import { getTemplateVersion, versionNotes, adaptSQLForServer, majorMinor } from '../src/versions';
|
|
6
|
+
import { findDifferences } from '../src/differences';
|
|
7
|
+
import { applyDifferences } from '../src/apply';
|
|
8
|
+
import { parseCreateTableSQL } from '../src/parsers';
|
|
9
|
+
|
|
10
|
+
const structure = (createSQL, procedures = []) => {
|
|
11
|
+
const parsed = parseCreateTableSQL(createSQL);
|
|
12
|
+
return { tables: { [parsed.name]: { ...parsed, triggers: [], createSQL } }, procedures };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// The same table as SHOW CREATE TABLE prints it on each version
|
|
16
|
+
const mysql57 = `CREATE TABLE \`Readings\` (
|
|
17
|
+
\`id\` int(11) NOT NULL AUTO_INCREMENT,
|
|
18
|
+
\`count\` bigint(20) unsigned NOT NULL DEFAULT '0',
|
|
19
|
+
\`active\` tinyint(1) NOT NULL DEFAULT '1',
|
|
20
|
+
\`level\` tinyint(4) DEFAULT NULL,
|
|
21
|
+
\`made\` year(4) DEFAULT NULL,
|
|
22
|
+
\`code\` int(5) unsigned zerofill DEFAULT NULL,
|
|
23
|
+
\`name\` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
|
|
24
|
+
PRIMARY KEY (\`id\`)
|
|
25
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
26
|
+
const mysql80 = `CREATE TABLE \`Readings\` (
|
|
27
|
+
\`id\` int NOT NULL AUTO_INCREMENT,
|
|
28
|
+
\`count\` bigint unsigned NOT NULL DEFAULT '0',
|
|
29
|
+
\`active\` tinyint(1) NOT NULL DEFAULT '1',
|
|
30
|
+
\`level\` tinyint DEFAULT NULL,
|
|
31
|
+
\`made\` year DEFAULT NULL,
|
|
32
|
+
\`code\` int(5) unsigned zerofill DEFAULT NULL,
|
|
33
|
+
\`name\` varchar(45) CHARACTER SET utf8mb3 DEFAULT NULL,
|
|
34
|
+
PRIMARY KEY (\`id\`)
|
|
35
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
|
|
36
|
+
|
|
37
|
+
describe('formatting that differs between MySQL 5.7 and 8.0', () => {
|
|
38
|
+
test('integer widths and year(4) are dropped, except tinyint(1) and zerofill', () => {
|
|
39
|
+
expect(normalizeVersionFormatting('int(11)')).toBe('int');
|
|
40
|
+
expect(normalizeVersionFormatting('bigint(20) unsigned')).toBe('bigint unsigned');
|
|
41
|
+
expect(normalizeVersionFormatting('tinyint(1)')).toBe('tinyint(1)');
|
|
42
|
+
expect(normalizeVersionFormatting('int(5) unsigned zerofill')).toBe('int(5) unsigned zerofill');
|
|
43
|
+
expect(normalizeVersionFormatting('year(4)')).toBe('year');
|
|
44
|
+
expect(normalizeVersionFormatting('varchar(10) CHARSET utf8mb3 COLLATE utf8mb3_bin')).toBe('varchar(10) CHARSET utf8 COLLATE utf8_bin');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a 5.7 table matches the same table on 8.0, both ways', () => {
|
|
48
|
+
expect(findDifferences(structure(mysql80 + ';'), structure(mysql57))).toEqual([]);
|
|
49
|
+
expect(findDifferences(structure(mysql57 + ';'), structure(mysql80))).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('real type changes are still reported', () => {
|
|
53
|
+
const changed = mysql57.replace('bigint(20) unsigned', 'bigint(20)').replace('tinyint(1) NOT NULL', 'tinyint(4) NOT NULL');
|
|
54
|
+
const diffs = findDifferences(structure(mysql80 + ';'), structure(changed));
|
|
55
|
+
expect(diffs.map(d => `${d.field.Field} ${d.info}`).sort()).toEqual(['active Type', 'count Type']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('procedures printed by each version match', () => {
|
|
59
|
+
const proc57 = 'CREATE DEFINER=`root`@`%` PROCEDURE `P`(IN a int(11), IN b varchar(10) CHARSET utf8)\nBEGIN\n SELECT a;\nEND';
|
|
60
|
+
const proc80 = 'CREATE DEFINER=`root`@`%` PROCEDURE `P`(IN a int, IN b varchar(10) CHARSET utf8mb3)\nBEGIN\n SELECT a;\nEND';
|
|
61
|
+
expect(findDifferences({ tables: {}, procedures: [{ Name: 'P', Definition: proc80 }] }, { tables: {}, procedures: [{ Name: 'P', Definition: proc57 }] })).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('template and server versions', () => {
|
|
66
|
+
const tempDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'mysqlsyncr-version-'));
|
|
67
|
+
const writeTemplate = (dir, createSQL) => {
|
|
68
|
+
fs.mkdirSync(path.join(dir, 'Alerts'));
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'Alerts', 'Alerts.json'), JSON.stringify(structure(createSQL)));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
test('uses the version dump recorded', async () => {
|
|
73
|
+
const dir = tempDir();
|
|
74
|
+
fs.writeFileSync(path.join(dir, 'dumpInfo.json'), JSON.stringify({ timestamp: 'x', mysqlVersion: '8.0.36' }));
|
|
75
|
+
expect(await getTemplateVersion(dir)).toEqual({ version: '8.0.36', inferred: false });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('infers it from older templates', async () => {
|
|
79
|
+
const dir57 = tempDir();
|
|
80
|
+
writeTemplate(dir57, mysql57.replace(/ CHARACTER SET utf8/, ''));
|
|
81
|
+
expect(await getTemplateVersion(dir57)).toMatchObject({ version: '5.7', inferred: true });
|
|
82
|
+
const dir80 = tempDir();
|
|
83
|
+
writeTemplate(dir80, mysql80);
|
|
84
|
+
expect(await getTemplateVersion(dir80)).toMatchObject({ version: '8.0', inferred: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('explains what differs between versions', () => {
|
|
88
|
+
expect(majorMinor('5.7.44-log')).toBe('5.7');
|
|
89
|
+
expect(versionNotes('8.0.36', '8.0.19')).toEqual([]);
|
|
90
|
+
expect(versionNotes('8.0', '5.7.44')).toHaveLength(3);
|
|
91
|
+
expect(versionNotes('5.7.44', '8.0.36')).toHaveLength(1);
|
|
92
|
+
expect(versionNotes(undefined, '8.0.36')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('applying an 8.0 template to a 5.7 server', () => {
|
|
97
|
+
test('8.0 collations are swapped for ones 5.7 has', () => {
|
|
98
|
+
expect(adaptSQLForServer('DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', '5.7.44')).toBe('DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci');
|
|
99
|
+
expect(adaptSQLForServer('COLLATE utf8mb4_0900_bin', '5.7.44')).toBe('COLLATE utf8mb4_bin');
|
|
100
|
+
expect(adaptSQLForServer('COLLATE utf8mb4_0900_ai_ci', '8.0.36')).toBe('COLLATE utf8mb4_0900_ai_ci');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('statements are adapted for the server they run on', async () => {
|
|
104
|
+
const statements = [];
|
|
105
|
+
const connection = {
|
|
106
|
+
query: async (sql) => {
|
|
107
|
+
if (sql.startsWith('SELECT VERSION')) return [{ version: '5.7.44-log' }];
|
|
108
|
+
if (!sql.startsWith('USE') && !sql.startsWith('SET SESSION')) statements.push(sql);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
await applyDifferences(connection, 'Alerts', [
|
|
113
|
+
{ type: 'missing_table', tableName: 'T', createSQL: 'CREATE TABLE `T` (\n `a` int\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci' }
|
|
114
|
+
]);
|
|
115
|
+
expect(statements).toEqual(['CREATE TABLE `T` (\n `a` int\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('when the server version is unknown', () => {
|
|
120
|
+
test('an unknown collation error is retried with the closest collation and reported as a warning', async () => {
|
|
121
|
+
const statements = [];
|
|
122
|
+
const connection = {
|
|
123
|
+
query: async (sql) => {
|
|
124
|
+
if (sql.startsWith('SELECT VERSION')) throw new Error('not allowed');
|
|
125
|
+
if (sql.startsWith('USE') || sql.startsWith('SET SESSION')) return [];
|
|
126
|
+
statements.push(sql);
|
|
127
|
+
if (sql.includes('utf8mb4_0900_ai_ci')) throw new Error("Unknown collation: 'utf8mb4_0900_ai_ci'");
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const result = await applyDifferences(connection, 'Alerts', [
|
|
132
|
+
{ type: 'missing_table', tableName: 'T', createSQL: 'CREATE TABLE `T` (\n `a` int\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci' }
|
|
133
|
+
]);
|
|
134
|
+
expect(statements[1]).toContain('COLLATE=utf8mb4_unicode_ci');
|
|
135
|
+
expect(result.failures).toEqual([]);
|
|
136
|
+
expect(result.warnings).toEqual([expect.objectContaining({ type: 'missing_table', tableName: 'T', message: expect.stringContaining('utf8mb4_unicode_ci') })]);
|
|
137
|
+
});
|
|
138
|
+
});
|