mysqlsyncr 1.0.0
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 +176 -0
- package/__tests__/parser.test.js +166 -0
- package/package.json +35 -0
- package/src/apply.js +187 -0
- package/src/cli.js +32 -0
- package/src/compare.js +80 -0
- package/src/create.js +15 -0
- package/src/differences.js +178 -0
- package/src/dump.js +12 -0
- package/src/getters.js +140 -0
- package/src/index.js +87 -0
- package/src/loggers.js +140 -0
- package/src/normalizers.js +6 -0
- package/src/parsers.js +129 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# MySQLSync
|
|
2
|
+
|
|
3
|
+
MySQLSync 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.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Global Installation](#global-installation)
|
|
10
|
+
- [Local Installation](#local-installation)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Commands](#commands)
|
|
13
|
+
- [Configuration Options](#configuration-options)
|
|
14
|
+
- [Using MySQLSync as a Module](#using-mysqlsync-as-a-module)
|
|
15
|
+
- [Contributing](#contributing)
|
|
16
|
+
- [License](#license)
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Dump**: Export the database structure to JSON files.
|
|
21
|
+
- **Compare**: Compare the current database structure with JSON dumps to identify differences.
|
|
22
|
+
- **Update**: Update the database structure based on the provided JSON definitions.
|
|
23
|
+
- **Verbose Logging**: Enable detailed logging for debugging purposes.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### Global Installation
|
|
28
|
+
|
|
29
|
+
To install MySQLSync globally, allowing you to use it from the command line, run the following command:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g mysqlsync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will allow you to run the command as `mysqlsync` from anywhere in your terminal.
|
|
36
|
+
|
|
37
|
+
### Local Installation
|
|
38
|
+
|
|
39
|
+
If you prefer to use MySQLSync locally within a project, you can install it as a dependency:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install mysqlsync
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This will add MySQLSync to your project's `node_modules` and allow you to use it within your project.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
You can run MySQLSync commands using the following syntax:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mysqlsync <command> [options]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Commands
|
|
56
|
+
|
|
57
|
+
- `dump`: Dumps the database structure to files.
|
|
58
|
+
- `compare`: Compares the database structure with JSON dumps (dry run).
|
|
59
|
+
- `update`: Updates the database structure based on JSON dumps.
|
|
60
|
+
|
|
61
|
+
### Configuration Options
|
|
62
|
+
|
|
63
|
+
You can configure the following options when running the commands:
|
|
64
|
+
|
|
65
|
+
| Option | Alias | Description | Default Value |
|
|
66
|
+
|-------------|-------|-----------------------------------------------------------------------------|--------------------------------|
|
|
67
|
+
| `host` | `-h` | Database host | `localhost` |
|
|
68
|
+
| `port` | `-P` | Database port | `3306` |
|
|
69
|
+
| `user` | `-u` | Database user | `root` |
|
|
70
|
+
| `password` | `-p` | Database password (required) | |
|
|
71
|
+
| `database` | `-d` | Target database | `null` |
|
|
72
|
+
| `output` | `-o` | Output directory for dumping. Default is one level up from the current directory in a new folder called `db-dump` | `../db-dump` |
|
|
73
|
+
| `verbose` | `-v` | Enable verbose logging (boolean) | `false` |
|
|
74
|
+
|
|
75
|
+
### Example Usage
|
|
76
|
+
|
|
77
|
+
1. **Dump the database structure**:
|
|
78
|
+
```bash
|
|
79
|
+
mysqlsync dump --host localhost --user root --password yourpassword
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. **Compare the database structure with JSON dumps**:
|
|
83
|
+
```bash
|
|
84
|
+
mysqlsync compare --host localhost --user root --password yourpassword
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. **Update the database structure based on JSON dumps**:
|
|
88
|
+
```bash
|
|
89
|
+
mysqlsync update --host localhost --user root --password yourpassword
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Using MySQLSync as a Module
|
|
93
|
+
|
|
94
|
+
If you want to use MySQLSync as a module in your own Node.js application, you can require it in your code:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
import mysqlsync from 'mysqlsync';
|
|
98
|
+
|
|
99
|
+
// Example usage
|
|
100
|
+
const config = {
|
|
101
|
+
host: 'localhost',
|
|
102
|
+
user: 'root',
|
|
103
|
+
password: 'yourpassword',
|
|
104
|
+
database: 'yourdatabase'
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
// app.js
|
|
110
|
+
import mysqlsync from 'mysqlsync';
|
|
111
|
+
import fs from 'fs';
|
|
112
|
+
import path from 'path';
|
|
113
|
+
|
|
114
|
+
// Database configuration
|
|
115
|
+
const config = {
|
|
116
|
+
host: 'localhost',
|
|
117
|
+
user: 'root',
|
|
118
|
+
password: 'yourpassword',
|
|
119
|
+
database: 'yourdatabase'
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Function to dump the database structure
|
|
123
|
+
async function dumpDatabase() {
|
|
124
|
+
try {
|
|
125
|
+
console.log('Dumping the database structure...');
|
|
126
|
+
await mysqlsync.dump(config);
|
|
127
|
+
console.log('Database structure dumped successfully.');
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Error dumping database:', error.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Function to compare the database structure with JSON dumps
|
|
134
|
+
async function compareDatabase() {
|
|
135
|
+
try {
|
|
136
|
+
const inputDir = path.join(__dirname, 'db-dump'); // Directory where JSON dumps are stored
|
|
137
|
+
console.log('Comparing the database structure with JSON dumps...');
|
|
138
|
+
const differences = await mysqlsync.compare(config, inputDir);
|
|
139
|
+
|
|
140
|
+
if (differences.length > 0) {
|
|
141
|
+
console.log('Differences found:');
|
|
142
|
+
console.log(differences);
|
|
143
|
+
} else {
|
|
144
|
+
console.log('No differences found. The database structure matches the JSON dumps.');
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Error comparing database:', error.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Main function to execute dump and compare
|
|
152
|
+
(async () => {
|
|
153
|
+
await dumpDatabase();
|
|
154
|
+
await compareDatabase();
|
|
155
|
+
})();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Make sure to check the source code for available functions and their usage.
|
|
159
|
+
|
|
160
|
+
## Contributing
|
|
161
|
+
|
|
162
|
+
Contributions are welcome! If you would like to contribute to MySQLSync, please follow these steps:
|
|
163
|
+
|
|
164
|
+
1. Fork the repository.
|
|
165
|
+
2. Create a new branch for your feature or bug fix.
|
|
166
|
+
3. Make your changes and commit them with clear messages.
|
|
167
|
+
4. Push your changes to your forked repository.
|
|
168
|
+
5. Open a pull request to the main repository.
|
|
169
|
+
|
|
170
|
+
### Code of Conduct
|
|
171
|
+
|
|
172
|
+
Please adhere to the [Code of Conduct](CODE_OF_CONDUCT.md) when contributing to this project.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
This project is licensed under the ISC License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { parseCreateTableSQL, parseDatabaseStructure } from '../src/parsers';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('Create Table Parser', () => {
|
|
6
|
+
|
|
7
|
+
test('should correctly parse a valid create table statement. Check #1', () => {
|
|
8
|
+
const input = `CREATE TABLE \`AlarmRuleHistory\` (
|
|
9
|
+
\`idAlarmHistory\` int(11) NOT NULL AUTO_INCREMENT,
|
|
10
|
+
\`idAsset\` int(11) NOT NULL,
|
|
11
|
+
\`idCaseDX\` int(11) DEFAULT NULL,
|
|
12
|
+
\`idAlertRule\` int(11) DEFAULT NULL,
|
|
13
|
+
\`activeAlarmRule\` smallint(1) NOT NULL DEFAULT '0',
|
|
14
|
+
\`alarmRule_StartDate\` timestamp NULL DEFAULT NULL,
|
|
15
|
+
\`alarmRule_EndDate\` timestamp NULL DEFAULT NULL,
|
|
16
|
+
\`dateCreated\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
+
\`dateUpdated\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
18
|
+
\`lastModifiedBy\` varchar(32) NOT NULL DEFAULT 'System Generated',
|
|
19
|
+
PRIMARY KEY (\`idAlarmHistory\`),
|
|
20
|
+
KEY \`AlertRule_idx\` (\`idAlertRule\`),
|
|
21
|
+
KEY \`CaseDX_idx\` (\`idCaseDX\`),
|
|
22
|
+
KEY \`Asset_idx\` (\`idAsset\`),
|
|
23
|
+
CONSTRAINT \`AlertRule\` FOREIGN KEY (\`idAlertRule\`) REFERENCES \`AlertRules\` (\`idAlertRule\`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
|
24
|
+
CONSTRAINT \`Asset\` FOREIGN KEY (\`idAsset\`) REFERENCES \`Assets\` (\`idAsset\`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
|
25
|
+
CONSTRAINT \`CaseDX\` FOREIGN KEY (\`idCaseDX\`) REFERENCES \`Case_DX\` (\`idCaseDX\`) ON DELETE NO ACTION ON UPDATE NO ACTION
|
|
26
|
+
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Keeps track of all active/inactive alarm rules used for the assets.\n';`;
|
|
27
|
+
|
|
28
|
+
const expectedOutput = {
|
|
29
|
+
"name": "AlarmRuleHistory",
|
|
30
|
+
"columns": [
|
|
31
|
+
{
|
|
32
|
+
"Field": "idAlarmHistory",
|
|
33
|
+
"Type": "int(11)",
|
|
34
|
+
"NotNull": true,
|
|
35
|
+
"AutoIncrement": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"Field": "idAsset",
|
|
39
|
+
"Type": "int(11)",
|
|
40
|
+
"NotNull": true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"Field": "idCaseDX",
|
|
44
|
+
"Type": "int(11)",
|
|
45
|
+
"Default": "NULL"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"Field": "idAlertRule",
|
|
49
|
+
"Type": "int(11)",
|
|
50
|
+
"Default": "NULL"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"Field": "activeAlarmRule",
|
|
54
|
+
"Type": "smallint(1)",
|
|
55
|
+
"NotNull": true,
|
|
56
|
+
"Default": "'0'"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"Field": "alarmRule_StartDate",
|
|
60
|
+
"Type": "timestamp",
|
|
61
|
+
"Default": "NULL"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"Field": "alarmRule_EndDate",
|
|
65
|
+
"Type": "timestamp",
|
|
66
|
+
"Default": "NULL"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"Field": "dateCreated",
|
|
70
|
+
"Type": "timestamp",
|
|
71
|
+
"NotNull": true,
|
|
72
|
+
"Default": "CURRENT_TIMESTAMP"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"Field": "dateUpdated",
|
|
76
|
+
"Type": "timestamp",
|
|
77
|
+
"NotNull": true,
|
|
78
|
+
"Default": "CURRENT_TIMESTAMP"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"Field": "lastModifiedBy",
|
|
82
|
+
"Type": "varchar(32)",
|
|
83
|
+
"NotNull": true,
|
|
84
|
+
"Default": "'System"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"indexes": [
|
|
88
|
+
{
|
|
89
|
+
"ColumnName": [
|
|
90
|
+
"`idAlarmHistory`"
|
|
91
|
+
],
|
|
92
|
+
"Primary": true
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"ColumnName": [
|
|
96
|
+
"`idAlertRule`"
|
|
97
|
+
],
|
|
98
|
+
"Name": "AlertRule_idx"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"ColumnName": [
|
|
102
|
+
"`idCaseDX`"
|
|
103
|
+
],
|
|
104
|
+
"Name": "CaseDX_idx"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"ColumnName": [
|
|
108
|
+
"`idAsset`"
|
|
109
|
+
],
|
|
110
|
+
"Name": "Asset_idx"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"constraints": [
|
|
114
|
+
{
|
|
115
|
+
"Name": "AlertRule",
|
|
116
|
+
"Type": "FOREIGN KEY",
|
|
117
|
+
"ColumnName": [
|
|
118
|
+
"`idAlertRule`"
|
|
119
|
+
],
|
|
120
|
+
"References": {
|
|
121
|
+
"Table": "`AlertRules`",
|
|
122
|
+
"Column": "`idAlertRule`"
|
|
123
|
+
},
|
|
124
|
+
"OnDelete": "NO ACTION",
|
|
125
|
+
"OnUpdate": "NO ACTION"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"Name": "AlertRule",
|
|
129
|
+
"Type": "FOREIGN KEY",
|
|
130
|
+
"ColumnName": [
|
|
131
|
+
"`idAsset`"
|
|
132
|
+
],
|
|
133
|
+
"References": {
|
|
134
|
+
"Table": "`Assets`",
|
|
135
|
+
"Column": "`idAsset`"
|
|
136
|
+
},
|
|
137
|
+
"OnDelete": "NO ACTION",
|
|
138
|
+
"OnUpdate": "NO ACTION"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"Name": "AlertRule",
|
|
142
|
+
"Type": "FOREIGN KEY",
|
|
143
|
+
"ColumnName": [
|
|
144
|
+
"`idCaseDX`"
|
|
145
|
+
],
|
|
146
|
+
"References": {
|
|
147
|
+
"Table": "`Case_DX`",
|
|
148
|
+
"Column": "`idCaseDX`"
|
|
149
|
+
},
|
|
150
|
+
"OnDelete": "NO ACTION",
|
|
151
|
+
"OnUpdate": "NO ACTION"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"engine": "InnoDB",
|
|
155
|
+
"charset": "latin1",
|
|
156
|
+
"collate": null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
const result = parseCreateTableSQL(input);
|
|
162
|
+
expect(result).toEqual(expectedOutput);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mysqlsyncr",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest",
|
|
9
|
+
"test:watch": "jest --watch",
|
|
10
|
+
"start": "node src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"mysqlsync": "src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"mysql2": "^3.12.0",
|
|
19
|
+
"yargs": "^17.7.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@babel/preset-env": "^7.26.0",
|
|
23
|
+
"babel-jest": "^29.7.0",
|
|
24
|
+
"jest": "^29.0.0"
|
|
25
|
+
},
|
|
26
|
+
"babel": {
|
|
27
|
+
"presets": ["@babel/preset-env"]
|
|
28
|
+
},
|
|
29
|
+
"jest": {
|
|
30
|
+
"transform": {
|
|
31
|
+
"^.+\\.js$": "babel-jest"
|
|
32
|
+
},
|
|
33
|
+
"testEnvironment": "node"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/apply.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { logger } from './loggers.js';
|
|
2
|
+
|
|
3
|
+
export const applyDifferences = async (connection, database, differences) => {
|
|
4
|
+
await connection.query(`USE \`${database}\``);
|
|
5
|
+
|
|
6
|
+
for (const diff of differences) {
|
|
7
|
+
try {
|
|
8
|
+
if (diff.type === 'missing_table') {
|
|
9
|
+
const { tableName, createSQL } = diff;
|
|
10
|
+
logger(`Creating missing table ${tableName}`);
|
|
11
|
+
logger(`Executing: ${createSQL}`);
|
|
12
|
+
await connection.query(createSQL);
|
|
13
|
+
|
|
14
|
+
} else if (diff.type === 'extra_table') {
|
|
15
|
+
const { tableName } = diff;
|
|
16
|
+
logger(`Dropping extra table ${tableName}`);
|
|
17
|
+
const dropTableSQL = `DROP TABLE \`${tableName}\`;`;
|
|
18
|
+
logger(`Executing: ${dropTableSQL}`);
|
|
19
|
+
await connection.query(dropTableSQL);
|
|
20
|
+
|
|
21
|
+
} else if (diff.type === 'missing_field') {
|
|
22
|
+
const defaultValue =
|
|
23
|
+
diff.field.Default === 'CURRENT_TIMESTAMP'
|
|
24
|
+
? 'DEFAULT CURRENT_TIMESTAMP'
|
|
25
|
+
: diff.field.Default !== null
|
|
26
|
+
? `DEFAULT '${diff.field.Default}'`
|
|
27
|
+
: '';
|
|
28
|
+
const addFieldSQL = `ALTER TABLE \`${diff.tableName}\` ADD \`${diff.field.Field}\` ${diff.field.Type} ${diff.field.Null === 'NO' ? 'NOT NULL' : ''
|
|
29
|
+
} ${defaultValue} ${diff.field.Extra || ''};`;
|
|
30
|
+
logger(`Executing: ${addFieldSQL}`);
|
|
31
|
+
await connection.query(addFieldSQL);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
} else if (diff.type === 'extra_field') {
|
|
35
|
+
const dropFieldSQL = `ALTER TABLE \`${diff.tableName}\` DROP COLUMN \`${diff.field.Field}\`;`;
|
|
36
|
+
logger(`Executing: ${dropFieldSQL}`);
|
|
37
|
+
await connection.query(dropFieldSQL);
|
|
38
|
+
|
|
39
|
+
} else if (diff.type === 'mismatched_field') {
|
|
40
|
+
const { tableName, field } = diff;
|
|
41
|
+
const dropFieldSQL = `ALTER TABLE \`${tableName}\` CHANGE \`${field.Field}\` \`${field.Field}\` ${field.Type} ${field.Null === 'NO' ? 'NOT NULL' : ''
|
|
42
|
+
} ${field.Default === 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP' : field.Default !== null ? `DEFAULT '${field.Default}'` : ''};`;
|
|
43
|
+
logger(`Executing: ${dropFieldSQL}`);
|
|
44
|
+
await connection.query(dropFieldSQL);
|
|
45
|
+
|
|
46
|
+
} else if (diff.type === 'missing_index') {
|
|
47
|
+
const existingColumns = await connection.query(`SHOW COLUMNS FROM \`${diff.tableName}\``);
|
|
48
|
+
const columnNames = existingColumns.map(col => col.Field);
|
|
49
|
+
|
|
50
|
+
// Check if all columns for the index exist
|
|
51
|
+
const missingColumns = Array.isArray(diff.index.ColumnName) ? diff.index.ColumnName.filter(col => !columnNames.includes(col)) : [];
|
|
52
|
+
|
|
53
|
+
if (missingColumns.length > 0) {
|
|
54
|
+
logger(`Cannot create index ${diff.index.Name} on ${diff.tableName}. Missing columns: ${missingColumns.join(', ')}`);
|
|
55
|
+
continue; // Skip index creation if columns are missing
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prepare the index columns with proper formatting
|
|
59
|
+
const indexColumns = diff.index.ColumnName.map(col => `\`${col}\``); // Format column names
|
|
60
|
+
|
|
61
|
+
// Handle PRIMARY index creation
|
|
62
|
+
if (diff.index.Name === 'PRIMARY') {
|
|
63
|
+
if (indexColumns.length > 0) {
|
|
64
|
+
const primaryKeySQL = `ALTER TABLE \`${diff.tableName}\` ADD PRIMARY KEY (${indexColumns.join(', ')});`;
|
|
65
|
+
logger(`Executing: ${primaryKeySQL}`);
|
|
66
|
+
await connection.query(primaryKeySQL);
|
|
67
|
+
} else {
|
|
68
|
+
logger(`Cannot create PRIMARY index on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Create the new index with UNIQUE if applicable
|
|
72
|
+
const uniqueSQL = diff.index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
|
|
73
|
+
if (indexColumns.length > 0) {
|
|
74
|
+
const addIndexSQL = `ALTER TABLE \`${diff.tableName}\` ADD ${uniqueSQL}INDEX \`${diff.index.Name}\` (${indexColumns.join(', ')});`;
|
|
75
|
+
logger(`Executing: ${addIndexSQL}`);
|
|
76
|
+
await connection.query(addIndexSQL);
|
|
77
|
+
} else {
|
|
78
|
+
logger(`Cannot create index ${diff.index.Name} on ${diff.tableName}. ColumnName is undefined or empty.`, diff.index);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
} else if (diff.type === 'extra_index') {
|
|
83
|
+
// Handle dropping of extra indexes
|
|
84
|
+
if (diff.index.Name === 'PRIMARY') {
|
|
85
|
+
const dropPrimaryKeySQL = `ALTER TABLE \`${diff.tableName}\` DROP PRIMARY KEY;`;
|
|
86
|
+
logger(`Executing: ${dropPrimaryKeySQL}`);
|
|
87
|
+
await connection.query(dropPrimaryKeySQL);
|
|
88
|
+
} else {
|
|
89
|
+
const dropIndexSQL = `DROP INDEX \`${diff.index.Name}\` ON \`${diff.tableName}\`;`;
|
|
90
|
+
logger(`Executing: ${dropIndexSQL}`);
|
|
91
|
+
await connection.query(dropIndexSQL);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} else if (diff.type === 'mismatched_index') {
|
|
95
|
+
const { tableName, index } = diff;
|
|
96
|
+
// Drop the existing index
|
|
97
|
+
if (index.Name === 'PRIMARY') {
|
|
98
|
+
const dropPrimaryKeySQL = `ALTER TABLE \`${tableName}\` DROP PRIMARY KEY;`;
|
|
99
|
+
logger(`Executing: ${dropPrimaryKeySQL}`);
|
|
100
|
+
await connection.query(dropPrimaryKeySQL);
|
|
101
|
+
} else {
|
|
102
|
+
const dropIndexSQL = `DROP INDEX \`${index.Name}\` ON \`${tableName}\`;`;
|
|
103
|
+
logger(`Executing: ${dropIndexSQL}`);
|
|
104
|
+
await connection.query(dropIndexSQL);
|
|
105
|
+
}
|
|
106
|
+
console.log('index', index)
|
|
107
|
+
|
|
108
|
+
// Create the new index
|
|
109
|
+
const uniqueSQL = index.Unique ? 'UNIQUE ' : ''; // Check if the index is unique
|
|
110
|
+
if (index.Name === 'PRIMARY') {
|
|
111
|
+
const addPrimaryKeySQL = `ALTER TABLE \`${tableName}\` ADD PRIMARY KEY (${index.ColumnName.join(', ')});`;
|
|
112
|
+
logger(`Executing: ${addPrimaryKeySQL}`);
|
|
113
|
+
await connection.query(addPrimaryKeySQL);
|
|
114
|
+
} else {
|
|
115
|
+
const addIndexSQL = `ALTER TABLE \`${tableName}\` ADD ${uniqueSQL}INDEX \`${index.Name}\` (${index.ColumnName.join(', ')});`; // Include UNIQUE if applicable
|
|
116
|
+
logger(`Executing: ${addIndexSQL}`);
|
|
117
|
+
await connection.query(addIndexSQL);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} else if (diff.type === 'missing_procedure' || diff.type === 'mismatched_procedure') {
|
|
121
|
+
|
|
122
|
+
// Drop the procedure if it exists
|
|
123
|
+
const dropCurrentProcSQL = `DROP PROCEDURE IF EXISTS \`${diff.Name}\`;`;
|
|
124
|
+
logger(`Executing: ${dropCurrentProcSQL}`);
|
|
125
|
+
await connection.query(dropCurrentProcSQL);
|
|
126
|
+
|
|
127
|
+
// Create the procedure
|
|
128
|
+
const createProcSQL = diff.Definition;
|
|
129
|
+
logger(`Executing: ${createProcSQL}`); // Log the SQL statement
|
|
130
|
+
await connection.query(createProcSQL);
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
} else if (diff.type === 'missing_view') {
|
|
135
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
136
|
+
logger(`Executing: ${dropViewSQL}`);
|
|
137
|
+
await connection.query(dropViewSQL);
|
|
138
|
+
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
139
|
+
logger(`Executing: ${createViewSQL}`);
|
|
140
|
+
await connection.query(createViewSQL);
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
} else if (diff.type === 'mismatched_view') {
|
|
144
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
145
|
+
logger(`Executing: ${dropViewSQL}`);
|
|
146
|
+
await connection.query(dropViewSQL);
|
|
147
|
+
const createViewSQL = diff.definition; // Use the expected definition to create the view
|
|
148
|
+
logger(`Executing: ${createViewSQL}`);
|
|
149
|
+
await connection.query(createViewSQL);
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
} else if (diff.type === 'extra_view') {
|
|
153
|
+
const dropViewSQL = `DROP VIEW IF EXISTS \`${diff.viewName}\`;`;
|
|
154
|
+
logger(`Executing: ${dropViewSQL}`);
|
|
155
|
+
await connection.query(dropViewSQL);
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
} else if (diff.type === 'missing_trigger') {
|
|
159
|
+
const createTriggerSQL = `CREATE TRIGGER \`${diff.triggerName}\` ${diff.definition.Event} ON \`${diff.definition.Table}\` FOR EACH ROW ${diff.definition.Definition}`;
|
|
160
|
+
logger(`Executing: ${createTriggerSQL}`);
|
|
161
|
+
await connection.query(createTriggerSQL);
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
} else if (diff.type === 'mismatched_trigger') {
|
|
165
|
+
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
166
|
+
logger(`Executing: ${dropTriggerSQL}`);
|
|
167
|
+
await connection.query(dropTriggerSQL);
|
|
168
|
+
const createTriggerSQL = `CREATE TRIGGER \`${diff.trigger.Name}\` ${diff.trigger.Event} ON \`${diff.trigger.Table}\` FOR EACH ROW ${diff.trigger.Definition}`;
|
|
169
|
+
logger(`Executing: ${createTriggerSQL}`);
|
|
170
|
+
await connection.query(createTriggerSQL);
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
} else if (diff.type === 'extra_trigger') {
|
|
174
|
+
const dropTriggerSQL = `DROP TRIGGER IF EXISTS \`${diff.trigger.Name}\`;`;
|
|
175
|
+
logger(`Executing: ${dropTriggerSQL}`);
|
|
176
|
+
await connection.query(dropTriggerSQL);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
} else {
|
|
181
|
+
logger(`Error applying difference of type:${diff.type}`, diff)
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
logger(`Error applying difference of type:${diff.type}. Error: ${e.message}`, diff);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { setVerbose } from './loggers.js';
|
|
4
|
+
|
|
5
|
+
export const cli = () => {
|
|
6
|
+
const argv = yargs(hideBin(process.argv))
|
|
7
|
+
.command('dump', 'Dump the database structure to files')
|
|
8
|
+
.command('compare', 'Compare the database structure with JSON dumps (dry run)')
|
|
9
|
+
.command('update', 'Update the database structure based on JSON dumps')
|
|
10
|
+
.option('host', { alias: 'h', describe: 'Database host', default: 'localhost' })
|
|
11
|
+
.option('port', { alias: 'P', describe: 'Database port', default: 3306 })
|
|
12
|
+
.option('user', { alias: 'u', describe: 'Database user', default: 'root' })
|
|
13
|
+
.option('password', { alias: 'p', describe: 'Database password', demandOption: true })
|
|
14
|
+
.option('database', { alias: 'd', describe: 'Target database', default: null })
|
|
15
|
+
.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' })
|
|
16
|
+
.option('verbose', { alias: 'v', describe: 'Enable verbose logging', type: 'boolean', default: false })
|
|
17
|
+
.demandCommand(1)
|
|
18
|
+
.help()
|
|
19
|
+
.argv;
|
|
20
|
+
|
|
21
|
+
const command = argv._[0];
|
|
22
|
+
const config = {
|
|
23
|
+
host: argv.host,
|
|
24
|
+
port: argv.port,
|
|
25
|
+
user: argv.user,
|
|
26
|
+
password: argv.password,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
setVerbose(argv.verbose);
|
|
30
|
+
|
|
31
|
+
return { command, config, argv };
|
|
32
|
+
};
|
package/src/compare.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getDatabases, getDatabaseStructure } from './getters.js';
|
|
4
|
+
import { findDifferences } from './differences.js';
|
|
5
|
+
import { logDifferences } from './loggers.js';
|
|
6
|
+
import { applyDifferences } from './apply.js';
|
|
7
|
+
|
|
8
|
+
export const compareAllDatabases = async (connection, inputDir, update = false, command, options) => {
|
|
9
|
+
if (connection.state === 'disconnected') {
|
|
10
|
+
await connection.reconnect(); // Ensure the connection is open
|
|
11
|
+
}
|
|
12
|
+
// Read the expected databases from the input directory
|
|
13
|
+
const expectedDatabases = await fs.readdir(inputDir);
|
|
14
|
+
// console.log('Expected databases from input directory:', expectedDatabases);
|
|
15
|
+
|
|
16
|
+
// Check existing databases
|
|
17
|
+
const existingDatabases = await getDatabases(connection);
|
|
18
|
+
// console.log('Existing databases:', existingDatabases);
|
|
19
|
+
|
|
20
|
+
const missingDatabases = [];
|
|
21
|
+
|
|
22
|
+
// Note missing databases and prepare to create them
|
|
23
|
+
for (const database of expectedDatabases) {
|
|
24
|
+
if(options.database && options.database !== database){
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!existingDatabases.includes(database)) {
|
|
28
|
+
missingDatabases.push(database);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create missing databases at the beginning
|
|
33
|
+
if (missingDatabases.length > 0 && update) {
|
|
34
|
+
console.log('Creating missing databases:');
|
|
35
|
+
for (const database of missingDatabases) {
|
|
36
|
+
console.log(`Creating database ${database}...`);
|
|
37
|
+
await connection.query(`CREATE DATABASE \`${database}\`;`);
|
|
38
|
+
console.log(`Database ${database} created.`);
|
|
39
|
+
}
|
|
40
|
+
}else if (missingDatabases.length > 0 && !update){
|
|
41
|
+
console.error('Missing databases found but update is not enabled. Please enable update to create missing databases.');
|
|
42
|
+
console.error('Please run the command again with the --update flag to create missing databases.');
|
|
43
|
+
console.error('Missing databases:');
|
|
44
|
+
console.error(missingDatabases);
|
|
45
|
+
return;
|
|
46
|
+
} else {
|
|
47
|
+
console.log('All expected databases exist.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const allDifferences = {};
|
|
51
|
+
for (const database of expectedDatabases) {
|
|
52
|
+
if(options.database && options.database !== database){
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const filePath = path.join(inputDir, database, `${database}.json`);
|
|
56
|
+
try {
|
|
57
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
58
|
+
const expectedStructure = JSON.parse(fileContent);
|
|
59
|
+
console.log('Getting database structure for', database);
|
|
60
|
+
const currentStructure = await getDatabaseStructure(connection, database, command, options);
|
|
61
|
+
console.log('Database structure for', database, 'obtained');
|
|
62
|
+
const differences = findDifferences(expectedStructure, currentStructure);
|
|
63
|
+
console.log('Differences for', database, 'obtained');
|
|
64
|
+
if (!update) {
|
|
65
|
+
console.log('Logging differences for', database);
|
|
66
|
+
logDifferences(differences, database, true);
|
|
67
|
+
} else {
|
|
68
|
+
console.log('Logging differences for', database);
|
|
69
|
+
logDifferences(differences, database, false);
|
|
70
|
+
console.log('Applying differences for', database);
|
|
71
|
+
await applyDifferences(connection, database, differences);
|
|
72
|
+
}
|
|
73
|
+
allDifferences[database] = differences;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Error processing database ${database}:`, err.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return allDifferences;
|
|
80
|
+
}
|