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/README.md CHANGED
@@ -1,186 +1,246 @@
1
- # MySQLSyncr
2
-
3
- Warning: Please be careful when using this tool. It is still in development and may not be fully stable. Test on a development database before using on production.
4
-
5
- MySQLSyncr is a command-line tool for managing and comparing MySQL database structures with JSON dumps. It allows users to dump the database structure, compare it with existing JSON files, and update the database based on those JSON definitions.
6
-
7
- ## Table of Contents
8
-
9
- - [Features](#features)
10
- - [Installation](#installation)
11
- - [Global Installation](#global-installation)
12
- - [Local Installation](#local-installation)
13
- - [Usage](#usage)
14
- - [Commands](#commands)
15
- - [Configuration Options](#configuration-options)
16
- - [Using MySQLSyncr as a Module](#using-mysqlsyncr-as-a-module)
17
- - [Contributing](#contributing)
18
- - [License](#license)
19
-
20
- ## Features
21
-
22
- - **Dump**: Export the database structure to JSON files.
23
- - **Compare**: Compare the current database structure with JSON dumps to identify differences.
24
- - **Update**: Update the database structure based on the provided JSON definitions.
25
- - **UI**: Open a web-based database browser interface.
26
- - **Verbose Logging**: Enable detailed logging for debugging purposes.
27
-
28
- ![Database Browser Interface](view.png)
29
-
30
- ## Installation
31
-
32
- ### Global Installation
33
-
34
- To install MySQLSync globally, allowing you to use it from the command line, run the following command:
35
-
36
- ```bash
37
- npm install -g mysqlsyncr
38
- ```
39
-
40
- This will allow you to run the command as `mysqlsyncr` from anywhere in your terminal.
41
-
42
- ### Local Installation
43
-
44
- If you prefer to use MySQLSyncr locally within a project, you can install it as a dependency:
45
-
46
- ```bash
47
- npm install mysqlsyncr
48
- ```
49
-
50
- This will add MySQLSyncr to your project's `node_modules` and allow you to use it within your project.
51
-
52
- ## Usage
53
-
54
- You can run MySQLSync commands using the following syntax:
55
-
56
- ```bash
57
- mysqlsyncr <command> [options]
58
- ```
59
-
60
- ### Commands
61
-
62
- - `dump`: Dumps the database structure to files.
63
- - `compare`: Compares the database structure with JSON dumps (dry run).
64
- - `update`: Updates the database structure based on JSON dumps.
65
- - `ui`: Opens a web-based database browser in your default system browser.
66
-
67
- ### Configuration Options
68
-
69
- You can configure the following options when running the commands:
70
-
71
- | Option | Alias | Description | Default Value |
72
- |-------------|-------|-----------------------------------------------------------------------------|--------------------------------|
73
- | `uiPort` | `-U` | UI port | `3600` |
74
- | `openUI` | `-O` | Open UI in default browser (boolean) | `true` |
75
- | `host` | `-h` | Database host | `localhost` |
76
- | `port` | `-P` | Database port | `3306` |
77
- | `user` | `-u` | Database user | `root` |
78
- | `password` | `-p` | Database password (required) | |
79
- | `database` | `-d` | Target database | `null` |
80
- | `output` | `-o` | Output directory for dumping. Default is one level up from the current directory in a new folder called `db-dump` | `../db-dump` |
81
- | `verbose` | `-v` | Enable verbose logging (boolean) | `false` |
82
-
83
- ### Example Usage
84
-
85
- 1. **Dump the database structure**:
86
- ```bash
87
- mysqlsyncr dump --host localhost --user root --password yourpassword
88
- ```
89
-
90
- 2. **Compare the database structure with JSON dumps**:
91
- ```bash
92
- mysqlsyncr compare --host localhost --user root --password yourpassword
93
- ```
94
-
95
- 3. **Update the database structure based on JSON dumps**:
96
- ```bash
97
- mysqlsyncr update --host localhost --user root --password yourpassword
98
- ```
99
-
100
- 4. **Open the database browser interface**:
101
- ```bash
102
- mysqlsyncr ui --host localhost --user root --password yourpassword
103
- ```
104
-
105
- ## Using MySQLSync as a Module
106
-
107
- If you want to use MySQLSync as a module in your own Node.js application, you can require it in your code:
108
-
109
- ```javascript
110
- import mysqlsyncr from 'mysqlsyncr';
111
-
112
- // Example usage
113
- const config = {
114
- host: 'localhost',
115
- user: 'root',
116
- password: 'yourpassword',
117
- database: 'yourdatabase'
118
- };
119
- ```
120
-
121
- ```javascript
122
- // app.js
123
- import mysqlsyncr from 'mysqlsyncr';
124
- import fs from 'fs';
125
- import path from 'path';
126
-
127
- // Database configuration
128
- const config = {
129
- host: 'localhost',
130
- user: 'root',
131
- password: 'yourpassword',
132
- database: 'yourdatabase'
133
- };
134
-
135
- // Function to dump the database structure
136
- async function dumpDatabase() {
137
- try {
138
- console.log('Dumping the database structure...');
139
- await mysqlsyncr.dump(config);
140
- console.log('Database structure dumped successfully.');
141
- } catch (error) {
142
- console.error('Error dumping database:', error.message);
143
- }
144
- }
145
-
146
- // Function to compare the database structure with JSON dumps
147
- async function compareDatabase() {
148
- try {
149
- const inputDir = path.join(__dirname, 'db-dump'); // Directory where JSON dumps are stored
150
- console.log('Comparing the database structure with JSON dumps...');
151
- const differences = await mysqlsyncr.compare(config, inputDir);
152
-
153
- if (differences.length > 0) {
154
- console.log('Differences found:');
155
- console.log(differences);
156
- } else {
157
- console.log('No differences found. The database structure matches the JSON dumps.');
158
- }
159
- } catch (error) {
160
- console.error('Error comparing database:', error.message);
161
- }
162
- }
163
-
164
- // Main function to execute dump and compare
165
- (async () => {
166
- await dumpDatabase();
167
- await compareDatabase();
168
- })();
169
- ```
170
-
171
- Make sure to check the source code for available functions and their usage.
172
-
173
- ## Contributing
174
-
175
- Contributions are welcome! If you would like to contribute to MySQLSyncr, please follow these steps:
176
-
177
- 1. Fork the repository.
178
- 2. Create a new branch for your feature or bug fix.
179
- 3. Make your changes and commit them with clear messages.
180
- 4. Push your changes to your forked repository.
181
- 5. Open a pull request to the main repository.
182
-
183
-
184
- ## License
185
-
186
- This project is licensed under the ISC License. See the [LICENSE](LICENSE) file for more details.
1
+ # MySQLSyncr
2
+
3
+ Warning: Please be careful when using this tool. It is still in development and may not be fully stable. Test on a development database before using on production.
4
+
5
+ MySQLSyncr is a command-line tool for managing and comparing MySQL database structures with JSON dumps. It allows users to dump the database structure, compare it with existing JSON files, and update the database based on those JSON definitions.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Installation](#installation)
11
+ - [Global Installation](#global-installation)
12
+ - [Local Installation](#local-installation)
13
+ - [Usage](#usage)
14
+ - [Commands](#commands)
15
+ - [Configuration Options](#configuration-options)
16
+ - [Using MySQLSyncr as a Module](#using-mysqlsyncr-as-a-module)
17
+ - [Contributing](#contributing)
18
+ - [License](#license)
19
+
20
+ ## Features
21
+
22
+ - **Dump**: Export the database structure to JSON files.
23
+ - **Compare**: Compare the current database structure with JSON dumps to identify differences.
24
+ - **Update**: Update the database structure based on the provided JSON definitions.
25
+ - **UI**: Open a web-based database browser interface.
26
+ - **Verbose Logging**: Enable detailed logging for debugging purposes.
27
+
28
+ ![Database Browser Interface](view.png)
29
+
30
+ ## Installation
31
+
32
+ ### Global Installation
33
+
34
+ To install MySQLSync globally, allowing you to use it from the command line, run the following command:
35
+
36
+ ```bash
37
+ npm install -g mysqlsyncr
38
+ ```
39
+
40
+ This will allow you to run the command as `mysqlsyncr` from anywhere in your terminal.
41
+
42
+ ### Local Installation
43
+
44
+ If you prefer to use MySQLSyncr locally within a project, you can install it as a dependency:
45
+
46
+ ```bash
47
+ npm install mysqlsyncr
48
+ ```
49
+
50
+ This will add MySQLSyncr to your project's `node_modules` and allow you to use it within your project.
51
+
52
+ ## Usage
53
+
54
+ You can run MySQLSync commands using the following syntax:
55
+
56
+ ```bash
57
+ mysqlsyncr <command> [options]
58
+ ```
59
+
60
+ ### Commands
61
+
62
+ - `dump`: Dumps the database structure to files.
63
+ - `compare`: Compares the database structure with JSON dumps (dry run).
64
+ - `update`: Updates the database structure based on JSON dumps.
65
+ - `ui`: Opens a web-based database browser in your default system browser.
66
+ - `rebuild`: Rebuilds each table's columns and indexes in the dump's JSON from its `CREATE TABLE` statement, and lists what changes. Dry run unless `--write` is given. Needs no database connection.
67
+
68
+ Run `mysqlsyncr` with no command in a terminal and it asks for the command and its options (host, port, user, password, database and folder) instead. The password isn't shown as you type, and `update` asks for confirmation before changing anything.
69
+
70
+ ### Configuration Options
71
+
72
+ You can configure the following options when running the commands:
73
+
74
+ | Option | Alias | Description | Default Value |
75
+ |-------------|-------|-----------------------------------------------------------------------------|--------------------------------|
76
+ | `uiPort` | `-U` | UI port | `3600` |
77
+ | `openUI` | `-O` | Open UI in default browser (boolean) | `true` |
78
+ | `host` | `-h` | Database host | `localhost` |
79
+ | `port` | `-P` | Database port | `3306` |
80
+ | `user` | `-u` | Database user | `root` |
81
+ | `password` | `-p` | Database password (required) | |
82
+ | `database` | `-d` | Target database | `null` |
83
+ | `output` | `-o` | Output directory for dumping. Default is one level up from the current directory in a new folder called `db-dump` | `../db-dump` |
84
+ | `verbose` | `-v` | Enable verbose logging (boolean) | `false` |
85
+ | `write` | | With `rebuild`, write the rebuilt files (boolean) | `false` |
86
+
87
+ ### Sync Options File
88
+
89
+ Put a `syncOptions.json` file in the dump directory (next to the database folders) to control table creation and seed rows. It's optional; without it everything behaves as before.
90
+
91
+ ```json
92
+ {
93
+ "definer": "root@%",
94
+ "autoIncrement": false,
95
+ "databases": {
96
+ "Alerts": {
97
+ "AlarmRules": {
98
+ "autoIncrement": 1000,
99
+ "rows": [
100
+ { "idAlarmRule": 1, "name": "Default rule" }
101
+ ]
102
+ },
103
+ "Settings": {
104
+ "rowKey": ["settingName"],
105
+ "rows": [
106
+ { "settingName": "emailEnabled", "value": "1" }
107
+ ]
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ **`autoIncrement`** sets the table's `AUTO_INCREMENT=N` counter in `CREATE TABLE`. The `AUTO_INCREMENT` attribute on the id column is never changed. Set it at the top level as the default for every table, and per table to override it.
115
+
116
+ | Value | Effect |
117
+ |-------|--------|
118
+ | `true` | Keep the counter as dumped (default) |
119
+ | `false` | Remove the counter, so new tables start at 1 |
120
+ | A number | Start new tables at that number |
121
+
122
+ It applies when `dump` saves a table, when `update` creates a missing table, and when `rebuild` rewrites a template. The counter is always ignored when comparing tables.
123
+
124
+ **`rows`** are seed rows. `compare` reports each row that isn't in the table as `missing_row`, and `update` inserts it after any table and column changes. A row counts as present when a row with the same `rowKey` values exists, so existing rows are never changed. `rowKey` defaults to the table's primary key; every row must include the key columns.
125
+
126
+ **`definer`** sets the `DEFINER` of every stored procedure, view and trigger, written as `root@%` or `` `root`@`%` ``. The template's definers are replaced with it before comparing, so an object in the database with a different definer is reported and `update` recreates it with this one. `dump` and Keep Current in the UI also save it. The MySQL user running the sync needs the `SET_USER_ID` (or `SUPER`) privilege to create objects for another definer, and the definer user should exist.
127
+
128
+ ### MySQL Versions
129
+
130
+ MySQL 5.7 and 8.0 print some definitions differently. These are formatting, not structure, so they're ignored when comparing tables, procedures, triggers and views:
131
+
132
+ - Integer display widths: 5.7 prints `int(11)`, 8.0.19 and later print `int`. `tinyint(1)` and `ZEROFILL` types keep their width on both.
133
+ - `YEAR(4)` is printed as `year` from 8.0.19.
134
+ - `utf8` is printed as `utf8mb3` from 8.0.24 (fully from 8.0.30).
135
+
136
+ `dump` records the server's version in `dumpInfo.json`. For templates dumped before that, the version is inferred from the SQL. `compare`, `update` and the UI show the template's and the server's versions and note what differs between them.
137
+
138
+ When applying a template from 8.0 to a 5.7 server, the `utf8mb4_0900_*` collations 5.7 doesn't have are replaced with `utf8mb4_unicode_ci` (`utf8mb4_bin` for accent- or case-sensitive ones), and each substitution is reported as a warning. Applying also sets `explicit_defaults_for_timestamp` for the session, as 8.0 does by default. Expression defaults such as `DEFAULT (...)` need 8.0.13 or later and fail on 5.7.
139
+
140
+ ### Example Usage
141
+
142
+ 1. **Dump the database structure**:
143
+ ```bash
144
+ mysqlsyncr dump --host localhost --user root --password yourpassword
145
+ ```
146
+
147
+ 2. **Compare the database structure with JSON dumps**:
148
+ ```bash
149
+ mysqlsyncr compare --host localhost --user root --password yourpassword
150
+ ```
151
+
152
+ 3. **Update the database structure based on JSON dumps**:
153
+ ```bash
154
+ mysqlsyncr update --host localhost --user root --password yourpassword
155
+ ```
156
+
157
+ 4. **Open the database browser interface**:
158
+ *Note: This does not work with PowerShell. Must use Command Prompt in windows*
159
+ ```bash
160
+ mysqlsyncr ui --host localhost --user root --password yourpassword
161
+ ```
162
+
163
+ ## Using MySQLSync as a Module
164
+
165
+ If you want to use MySQLSync as a module in your own Node.js application, you can require it in your code:
166
+
167
+ ```javascript
168
+ import mysqlsyncr from 'mysqlsyncr';
169
+
170
+ // Example usage
171
+ const config = {
172
+ host: 'localhost',
173
+ user: 'root',
174
+ password: 'yourpassword',
175
+ database: 'yourdatabase'
176
+ };
177
+ ```
178
+
179
+ ```javascript
180
+ // app.js
181
+ import mysqlsyncr from 'mysqlsyncr';
182
+ import fs from 'fs';
183
+ import path from 'path';
184
+
185
+ // Database configuration
186
+ const config = {
187
+ host: 'localhost',
188
+ user: 'root',
189
+ password: 'yourpassword',
190
+ database: 'yourdatabase'
191
+ };
192
+
193
+ // Function to dump the database structure
194
+ async function dumpDatabase() {
195
+ try {
196
+ console.log('Dumping the database structure...');
197
+ await mysqlsyncr.dump(config);
198
+ console.log('Database structure dumped successfully.');
199
+ } catch (error) {
200
+ console.error('Error dumping database:', error.message);
201
+ }
202
+ }
203
+
204
+ // Function to compare the database structure with JSON dumps
205
+ async function compareDatabase() {
206
+ try {
207
+ const inputDir = path.join(__dirname, 'db-dump'); // Directory where JSON dumps are stored
208
+ console.log('Comparing the database structure with JSON dumps...');
209
+ const differences = await mysqlsyncr.compare(config, inputDir);
210
+
211
+ if (differences.length > 0) {
212
+ console.log('Differences found:');
213
+ console.log(differences);
214
+ } else {
215
+ console.log('No differences found. The database structure matches the JSON dumps.');
216
+ }
217
+ } catch (error) {
218
+ console.error('Error comparing database:', error.message);
219
+ }
220
+ }
221
+
222
+ // Main function to execute dump and compare
223
+ (async () => {
224
+ await dumpDatabase();
225
+ await compareDatabase();
226
+ })();
227
+ ```
228
+
229
+ Make sure to check the source code for available functions and their usage.
230
+
231
+ ## Contributing
232
+
233
+ Contributions are welcome! If you would like to contribute to MySQLSyncr, please follow these steps:
234
+
235
+ 1. Fork the repository.
236
+ 2. Create a new branch for your feature or bug fix.
237
+ 3. Make your changes and commit them with clear messages.
238
+ 4. Push your changes to your forked repository.
239
+ 5. Open a pull request to the main repository.
240
+
241
+ Before committing, run `npm test` and `npm run build`, which rebuilds `dist/` from `src/`. If you changed the UI (`ui/src`), also rebuild `ui/dist` with `npm run build` in `ui/`. That needs Node.js 22.22.0 or later; see [ui/README.md](ui/README.md). Running or installing mysqlsyncr has no such requirement.
242
+
243
+
244
+ ## License
245
+
246
+ This project is licensed under the ISC License. See the [LICENSE](LICENSE) file for more details.
@@ -0,0 +1,117 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { replaceCreateSQLElement, acceptCurrentIntoTemplate } from '../src/acceptCurrent';
5
+ import { parseCreateTableSQL } from '../src/parsers';
6
+ import { findDifferences } from '../src/differences';
7
+
8
+ const templateSQL = `CREATE TABLE \`E2Data\` (
9
+ \`IID\` varchar(25) DEFAULT NULL,
10
+ \`DeviceId\` varchar(45) NOT NULL,
11
+ \`oldColumn\` int DEFAULT NULL,
12
+ PRIMARY KEY (\`IID\`),
13
+ KEY \`IndexIID\` (\`IID\`)
14
+ ) ENGINE=InnoDB DEFAULT CHARSET=latin1`;
15
+
16
+ const liveSQL = `CREATE TABLE \`E2Data\` (
17
+ \`IID\` varchar(25) DEFAULT NULL,
18
+ \`DeviceId\` varchar(75) NOT NULL,
19
+ \`newColumn\` int NOT NULL DEFAULT '0',
20
+ PRIMARY KEY (\`IID\`),
21
+ KEY \`IndexIID\` (\`IID\`,\`DeviceId\`),
22
+ KEY \`idx_new\` (\`newColumn\`)
23
+ ) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=latin1`;
24
+
25
+ describe('replaceCreateSQLElement', () => {
26
+ test('replaces a changed column line', () => {
27
+ const result = replaceCreateSQLElement(templateSQL, liveSQL, 'col:DeviceId');
28
+ expect(result).toContain(' `DeviceId` varchar(75) NOT NULL,\n');
29
+ expect(result.split('\n')).toHaveLength(templateSQL.split('\n').length);
30
+ });
31
+
32
+ test('removes a column the database does not have', () => {
33
+ expect(replaceCreateSQLElement(templateSQL, liveSQL, 'col:oldColumn')).not.toContain('oldColumn');
34
+ });
35
+
36
+ test('adds a column after the one before it in the database', () => {
37
+ const result = replaceCreateSQLElement(templateSQL, liveSQL, 'col:newColumn');
38
+ expect(result).toContain(" `DeviceId` varchar(45) NOT NULL,\n `newColumn` int NOT NULL DEFAULT '0',\n `oldColumn`");
39
+ });
40
+
41
+ test('adds, replaces and removes indexes, keeping commas valid', () => {
42
+ let result = replaceCreateSQLElement(templateSQL, liveSQL, 'idx:idx_new');
43
+ expect(result).toContain(' KEY `IndexIID` (`IID`),\n KEY `idx_new` (`newColumn`)\n) ENGINE');
44
+ result = replaceCreateSQLElement(result, liveSQL, 'idx:IndexIID');
45
+ expect(result).toContain('KEY `IndexIID` (`IID`,`DeviceId`),');
46
+ result = replaceCreateSQLElement(result, templateSQL, 'idx:idx_new');
47
+ expect(result).toContain(' KEY `IndexIID` (`IID`,`DeviceId`)\n) ENGINE');
48
+ expect(parseCreateTableSQL(result).indexes.map(i => i.Name)).toEqual(['PRIMARY', 'IndexIID']);
49
+ });
50
+
51
+ test('leaves the AUTO_INCREMENT counter of the template alone', () => {
52
+ expect(replaceCreateSQLElement(templateSQL, liveSQL, 'col:DeviceId')).not.toContain('AUTO_INCREMENT=');
53
+ });
54
+ });
55
+
56
+ describe('acceptCurrentIntoTemplate', () => {
57
+ // Answers the queries getDatabaseStructure makes for a database holding liveSQL
58
+ const liveConnection = () => ({
59
+ query: async (sql) => {
60
+ if (sql.startsWith('SHOW FULL TABLES WHERE')) return [{ Tables_in_db: 'E2Data' }];
61
+ if (sql.startsWith('SHOW CREATE TABLE')) return [{ 'Create Table': liveSQL }];
62
+ return [];
63
+ }
64
+ });
65
+
66
+ const writeTemplate = () => {
67
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mysqlsyncr-accept-'));
68
+ fs.mkdirSync(path.join(dir, 'currentCOMPortData', 'tables'), { recursive: true });
69
+ const parsed = parseCreateTableSQL(templateSQL);
70
+ const template = {
71
+ tables: {
72
+ E2Data: { columns: parsed.columns, indexes: parsed.indexes, constraints: [], triggers: [], createSQL: templateSQL },
73
+ Dropped: { columns: [], indexes: [], constraints: [], triggers: [], createSQL: 'CREATE TABLE `Dropped` (\n `a` int\n)' }
74
+ },
75
+ procedures: []
76
+ };
77
+ fs.writeFileSync(path.join(dir, 'currentCOMPortData', 'currentCOMPortData.json'), JSON.stringify(template));
78
+ fs.writeFileSync(path.join(dir, 'currentCOMPortData', 'tables', 'Dropped.sql'), 'x');
79
+ return dir;
80
+ };
81
+ const readTemplate = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'currentCOMPortData', 'currentCOMPortData.json'), 'utf-8'));
82
+
83
+ test('keeping every difference leaves none', async () => {
84
+ const dir = writeTemplate();
85
+ const live = { tables: { E2Data: { ...parseCreateTableSQL(liveSQL), triggers: [], createSQL: liveSQL } }, procedures: [] };
86
+ const diffs = findDifferences(readTemplate(dir), live);
87
+ expect(diffs.map(d => d.type).sort()).toEqual(['extra_field', 'extra_index', 'extra_index', 'missing_field', 'missing_table', 'mismatched_field'].sort());
88
+
89
+ await acceptCurrentIntoTemplate(liveConnection(), dir, 'currentCOMPortData', diffs);
90
+
91
+ const template = readTemplate(dir);
92
+ expect(findDifferences(template, live)).toEqual([]);
93
+ expect(template.tables.Dropped).toBeUndefined();
94
+ expect(fs.existsSync(path.join(dir, 'currentCOMPortData', 'tables', 'Dropped.sql'))).toBe(false);
95
+ expect(fs.readFileSync(path.join(dir, 'currentCOMPortData', 'tables', 'E2Data.sql'), 'utf-8')).toBe(`${template.tables.E2Data.createSQL}\n`);
96
+ });
97
+
98
+ test('only the chosen difference changes', async () => {
99
+ const dir = writeTemplate();
100
+ await acceptCurrentIntoTemplate(liveConnection(), dir, 'currentCOMPortData', [
101
+ { type: 'mismatched_field', info: 'Type', tableName: 'E2Data', field: { Field: 'DeviceId', Type: 'varchar(45)', NotNull: true } }
102
+ ]);
103
+ const table = readTemplate(dir).tables.E2Data;
104
+ expect(table.columns.map(c => c.Field)).toEqual(['IID', 'DeviceId', 'oldColumn']);
105
+ expect(table.columns[1].Type).toBe('varchar(75)');
106
+ expect(table.indexes.find(i => i.Name === 'IndexIID').ColumnName).toEqual(['IID']);
107
+ });
108
+
109
+ test('saves what it can and reports the rest', async () => {
110
+ const dir = writeTemplate();
111
+ await expect(acceptCurrentIntoTemplate(liveConnection(), dir, 'currentCOMPortData', [
112
+ { type: 'missing_row', tableName: 'E2Data', key: { IID: 'x' }, row: { IID: 'x' } },
113
+ { type: 'missing_table', tableName: 'Dropped' }
114
+ ])).rejects.toMatchObject({ errors: [{ difference: 'missing_row E2Data' }] });
115
+ expect(readTemplate(dir).tables.Dropped).toBeUndefined();
116
+ });
117
+ });
@@ -0,0 +1,101 @@
1
+ import { parseCreateTableSQL } from '../src/parsers';
2
+
3
+ // Regression tests for column attributes rendered between the type and the
4
+ // NULL/DEFAULT clauses. Before the fix these columns parsed with no Default and
5
+ // no NotNull, which produced phantom `mismatched_field` differences whenever the
6
+ // two sides of a compare rendered COLLATE differently. The parser also records
7
+ // the collation, charset, unsigned and generated expression, so a column can be
8
+ // restated without losing them.
9
+
10
+ const columnOf = (sql, name) =>
11
+ parseCreateTableSQL(sql).columns.find(c => c.Field === name);
12
+
13
+ describe('Column attributes between type and DEFAULT', () => {
14
+
15
+ test('COLLATE does not swallow DEFAULT NULL', () => {
16
+ const input = `CREATE TABLE \`E2Alarms\` (
17
+ \`IID\` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
18
+ PRIMARY KEY (\`IID\`)
19
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`;
20
+
21
+ expect(columnOf(input, 'IID')).toEqual({
22
+ Field: 'IID',
23
+ Type: 'varchar(25)',
24
+ Collate: 'utf8mb4_unicode_ci',
25
+ Default: 'NULL'
26
+ });
27
+ });
28
+
29
+ test('a collated column parses the same with or without the COLLATE clause, apart from the collation', () => {
30
+ const withCollate = `CREATE TABLE \`T\` (
31
+ \`point\` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL
32
+ )`;
33
+ const withoutCollate = `CREATE TABLE \`T\` (
34
+ \`point\` varchar(45) DEFAULT NULL
35
+ )`;
36
+
37
+ const { Collate, ...rest } = columnOf(withCollate, 'point');
38
+ expect(Collate).toBe('utf8mb4_unicode_ci');
39
+ expect(rest).toEqual(columnOf(withoutCollate, 'point'));
40
+ });
41
+
42
+ test('CHARACTER SET and COLLATE together do not swallow NOT NULL or DEFAULT', () => {
43
+ const input = `CREATE TABLE \`T\` (
44
+ \`name\` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'x'
45
+ )`;
46
+
47
+ expect(columnOf(input, 'name')).toEqual({
48
+ Field: 'name',
49
+ Type: 'varchar(45)',
50
+ Charset: 'utf8mb4',
51
+ Collate: 'utf8mb4_unicode_ci',
52
+ NotNull: true,
53
+ Default: 'x'
54
+ });
55
+ });
56
+
57
+ test('unsigned does not swallow NOT NULL, DEFAULT or AUTO_INCREMENT, and stays part of the type', () => {
58
+ const input = `CREATE TABLE \`T\` (
59
+ \`qty\` int(11) unsigned NOT NULL DEFAULT '0',
60
+ \`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT
61
+ )`;
62
+
63
+ expect(columnOf(input, 'qty')).toEqual({
64
+ Field: 'qty',
65
+ Type: 'int(11) unsigned',
66
+ NotNull: true,
67
+ Default: '0'
68
+ });
69
+ expect(columnOf(input, 'id')).toEqual({
70
+ Field: 'id',
71
+ Type: 'bigint(20) unsigned',
72
+ NotNull: true,
73
+ AutoIncrement: true
74
+ });
75
+ });
76
+
77
+ test('a collated column with no DEFAULT still parses without one', () => {
78
+ const input = `CREATE TABLE \`T\` (
79
+ \`alarmString\` text COLLATE utf8mb4_unicode_ci
80
+ )`;
81
+
82
+ expect(columnOf(input, 'alarmString')).toEqual({
83
+ Field: 'alarmString',
84
+ Type: 'text',
85
+ Collate: 'utf8mb4_unicode_ci'
86
+ });
87
+ });
88
+
89
+ test('a collated generated column is not given a DEFAULT', () => {
90
+ const input = `CREATE TABLE \`T\` (
91
+ \`computed_tablename\` varchar(255) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (concat(_utf8mb4'E2Data||',\`IID\`,_utf8mb4'||',\`point\`)) STORED
92
+ )`;
93
+
94
+ expect(columnOf(input, 'computed_tablename')).toEqual({
95
+ Field: 'computed_tablename',
96
+ Type: 'varchar(255)',
97
+ Collate: 'utf8mb4_unicode_ci',
98
+ Generated: "(concat(_utf8mb4'E2Data||',`IID`,_utf8mb4'||',`point`)) STORED"
99
+ });
100
+ });
101
+ });