mysqlsyncr 1.1.12 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/__tests__/acceptCurrent.test.js +117 -0
- package/__tests__/columnAttributes.test.js +101 -0
- package/__tests__/options.test.js +259 -0
- package/__tests__/parser.test.js +167 -166
- package/__tests__/parser2.test.js +155 -152
- package/__tests__/paths.test.js +21 -0
- package/__tests__/prompt.test.js +106 -0
- package/__tests__/sync.test.js +348 -0
- package/__tests__/templateDrift.test.js +72 -0
- package/__tests__/versions.test.js +138 -0
- package/dist/index.cjs +4059 -1733
- package/dist/index.js +4101 -1777
- package/package.json +66 -59
- package/rollup.config.js +2 -1
- package/src/DBStructureChecker.js +47 -0
- package/src/acceptCurrent.js +225 -0
- package/src/apply.js +254 -136
- package/src/cli.js +61 -34
- package/src/compare.js +106 -92
- package/src/differences.js +258 -212
- package/src/dump.js +63 -54
- package/src/getters.js +151 -139
- package/src/index.js +223 -169
- package/src/loggers.js +156 -139
- package/src/normalizers.js +28 -6
- package/src/options.js +133 -0
- package/src/parsers.js +82 -36
- package/src/paths.js +41 -0
- package/src/prompt.js +109 -0
- package/src/rebuild.js +106 -0
- package/src/rows.js +46 -0
- package/src/socketServer.js +106 -24
- package/src/templateDrift.js +93 -0
- package/src/versions.js +112 -0
- package/ui/README.md +55 -40
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
- package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
- package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
- package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
- package/ui/dist/spa/index.html +2 -2
- package/ui/package.json +43 -42
- package/ui/src/components/DefinitionDiff.vue +169 -0
- package/ui/src/pages/MainComponent.vue +704 -78
- package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
- package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
- package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
- package/ui/dist/spa/assets/index-kXpA8n8r.css +0 -5
package/src/loggers.js
CHANGED
|
@@ -1,140 +1,157 @@
|
|
|
1
|
-
let isVerbose = false; // Global variable to manage verbosity
|
|
2
|
-
|
|
3
|
-
export const setVerbose = (verbose) => {
|
|
4
|
-
isVerbose = verbose; // Function to set the verbosity level
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const logger = (...args) => {
|
|
8
|
-
if (isVerbose) {
|
|
9
|
-
console.log(...args); // Log all arguments if verbose is enabled
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const logDifferences = (differences, database, dryRun) => {
|
|
14
|
-
const formattedDifferences = differences.map(diff => {
|
|
15
|
-
switch (diff.type) {
|
|
16
|
-
case 'missing_procedure':
|
|
17
|
-
return {
|
|
18
|
-
Type: diff.type,
|
|
19
|
-
Name: diff.Name,
|
|
20
|
-
Details: `Procedure ${diff.Name} is missing`,
|
|
21
|
-
Action: 'Procedure is missing',
|
|
22
|
-
};
|
|
23
|
-
case 'mismatched_procedure':
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
Type: diff.type,
|
|
27
|
-
Name: diff.Name,
|
|
28
|
-
Details: `Mismatched procedure ${diff.Name}`,
|
|
29
|
-
Action: 'Mismatched procedure',
|
|
30
|
-
};
|
|
31
|
-
case 'missing_field':
|
|
32
|
-
return {
|
|
33
|
-
Type: diff.type,
|
|
34
|
-
Name: diff.tableName,
|
|
35
|
-
Details: `Field ${diff.field.Field} is missing`,
|
|
36
|
-
Action: 'Field is missing',
|
|
37
|
-
};
|
|
38
|
-
case '
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Details: `Index ${diff.index.Name} is
|
|
89
|
-
Action: 'Index is
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
case '
|
|
93
|
-
return {
|
|
94
|
-
Type: diff.type,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1
|
+
let isVerbose = false; // Global variable to manage verbosity
|
|
2
|
+
|
|
3
|
+
export const setVerbose = (verbose) => {
|
|
4
|
+
isVerbose = verbose; // Function to set the verbosity level
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const logger = (...args) => {
|
|
8
|
+
if (isVerbose) {
|
|
9
|
+
console.log(...args); // Log all arguments if verbose is enabled
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const logDifferences = (differences, database, dryRun) => {
|
|
14
|
+
const formattedDifferences = differences.map(diff => {
|
|
15
|
+
switch (diff.type) {
|
|
16
|
+
case 'missing_procedure':
|
|
17
|
+
return {
|
|
18
|
+
Type: diff.type,
|
|
19
|
+
Name: diff.Name,
|
|
20
|
+
Details: `Procedure ${diff.Name} is missing`,
|
|
21
|
+
Action: 'Procedure is missing',
|
|
22
|
+
};
|
|
23
|
+
case 'mismatched_procedure':
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
Type: diff.type,
|
|
27
|
+
Name: diff.Name,
|
|
28
|
+
Details: `Mismatched procedure ${diff.Name}`,
|
|
29
|
+
Action: 'Mismatched procedure',
|
|
30
|
+
};
|
|
31
|
+
case 'missing_field':
|
|
32
|
+
return {
|
|
33
|
+
Type: diff.type,
|
|
34
|
+
Name: diff.tableName,
|
|
35
|
+
Details: `Field ${diff.field.Field} is missing`,
|
|
36
|
+
Action: 'Field is missing',
|
|
37
|
+
};
|
|
38
|
+
case 'mismatched_field': {
|
|
39
|
+
// `info` names the check that failed; Null is stored as NotNull.
|
|
40
|
+
const property = diff.info === 'Null' ? 'NotNull' : diff.info;
|
|
41
|
+
return {
|
|
42
|
+
Type: diff.type,
|
|
43
|
+
Name: diff.tableName,
|
|
44
|
+
Details: `Field ${diff.field.Field} ${diff.info} is ${JSON.stringify(diff.currentField?.[property])}, expected ${JSON.stringify(diff.field?.[property])}`,
|
|
45
|
+
Action: 'Field is mismatched',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case 'missing_table':
|
|
49
|
+
return {
|
|
50
|
+
Type: diff.type,
|
|
51
|
+
Name: diff.tableName,
|
|
52
|
+
Details: `Table ${diff.tableName} is missing`,
|
|
53
|
+
Action: 'Table is missing',
|
|
54
|
+
};
|
|
55
|
+
case 'extra_field':
|
|
56
|
+
return {
|
|
57
|
+
Type: diff.type,
|
|
58
|
+
Name: diff.tableName,
|
|
59
|
+
Details: `Field ${diff.field.Field} is extra`,
|
|
60
|
+
Action: 'Field is extra',
|
|
61
|
+
};
|
|
62
|
+
case 'extra_table':
|
|
63
|
+
return {
|
|
64
|
+
Type: diff.type,
|
|
65
|
+
Name: diff.tableName,
|
|
66
|
+
Details: `Table ${diff.tableName} is extra`,
|
|
67
|
+
Action: 'Table is extra',
|
|
68
|
+
};
|
|
69
|
+
case 'missing_view':
|
|
70
|
+
return {
|
|
71
|
+
Type: diff.type,
|
|
72
|
+
Name: diff.viewName,
|
|
73
|
+
Details: `View ${diff.viewName} is missing`,
|
|
74
|
+
Action: 'View is missing',
|
|
75
|
+
};
|
|
76
|
+
case 'extra_index':
|
|
77
|
+
return {
|
|
78
|
+
Type: diff.type,
|
|
79
|
+
Name: diff.tableName,
|
|
80
|
+
Details: `Index ${diff.index.Name} is extra`,
|
|
81
|
+
Action: 'Index is extra',
|
|
82
|
+
};
|
|
83
|
+
case 'missing_index':
|
|
84
|
+
return {
|
|
85
|
+
Type: diff.type,
|
|
86
|
+
TableName: diff.tableName,
|
|
87
|
+
Name: diff.index.Name,
|
|
88
|
+
Details: `Index ${diff.index.Name} is missing`,
|
|
89
|
+
Action: 'Index is missing',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'mismatched_index':
|
|
93
|
+
return {
|
|
94
|
+
Type: diff.type,
|
|
95
|
+
TableName: diff.tableName,
|
|
96
|
+
Name: diff.index.Name,
|
|
97
|
+
From: diff.from,
|
|
98
|
+
Details: `Index ${diff.index.Name} is mismatched`,
|
|
99
|
+
Action: 'Index is mismatched',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
case 'extra_trigger':
|
|
103
|
+
return {
|
|
104
|
+
Type: diff.type,
|
|
105
|
+
Name: diff.trigger.Name,
|
|
106
|
+
Details: `Trigger ${diff.trigger.Name} is extra`,
|
|
107
|
+
Action: 'Trigger is extra',
|
|
108
|
+
};
|
|
109
|
+
case 'mismatched_trigger':
|
|
110
|
+
return {
|
|
111
|
+
Type: diff.type,
|
|
112
|
+
Name: diff.trigger.Name,
|
|
113
|
+
Details: `Trigger ${diff.trigger.Name} is mismatched`,
|
|
114
|
+
Action: 'Trigger is mismatched',
|
|
115
|
+
};
|
|
116
|
+
case 'missing_trigger':
|
|
117
|
+
logger('missing_trigger', diff)
|
|
118
|
+
return {
|
|
119
|
+
Type: diff.type,
|
|
120
|
+
Name: diff.trigger.Name,
|
|
121
|
+
Details: `Trigger ${diff.trigger.Name} is missing`,
|
|
122
|
+
Action: 'Trigger is missing',
|
|
123
|
+
};
|
|
124
|
+
case 'missing_row':
|
|
125
|
+
return {
|
|
126
|
+
Type: diff.type,
|
|
127
|
+
Name: diff.tableName,
|
|
128
|
+
Details: `Row ${JSON.stringify(diff.key)} is missing`,
|
|
129
|
+
Action: 'Row is missing',
|
|
130
|
+
};
|
|
131
|
+
default:
|
|
132
|
+
return {
|
|
133
|
+
Type: diff.type,
|
|
134
|
+
Details: `Unhandled difference type: ${diff.type}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Filter out any undefined or null entries
|
|
140
|
+
const filteredDifferences = formattedDifferences.filter(diff => diff);
|
|
141
|
+
|
|
142
|
+
// Log the differences in a table format
|
|
143
|
+
if (filteredDifferences.length > 0) {
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
logger(`Dry Run: Differences for database ${database}:`);
|
|
146
|
+
} else {
|
|
147
|
+
logger(`Differences applied for database ${database}:`);
|
|
148
|
+
}
|
|
149
|
+
console.table(filteredDifferences);
|
|
150
|
+
} else {
|
|
151
|
+
if (dryRun) {
|
|
152
|
+
logger(`Dry Run: No differences found. Database: ${database}`);
|
|
153
|
+
} else {
|
|
154
|
+
logger(`No differences found. Database: ${database}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
140
157
|
}
|
package/src/normalizers.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Undo formatting that differs between MySQL versions but doesn't change the
|
|
3
|
+
* structure, so a template dumped from one version compares cleanly with
|
|
4
|
+
* another:
|
|
5
|
+
* - integer display widths: int(11) in 5.7 is int from 8.0.19, except
|
|
6
|
+
* tinyint(1) and ZEROFILL types, which keep them
|
|
7
|
+
* - year(4) in 5.7 is year from 8.0.19
|
|
8
|
+
* - utf8 is shown as utf8mb3 from 8.0.24 (fully from 8.0.30)
|
|
9
|
+
* Applies to column types and to procedure, trigger and view definitions.
|
|
10
|
+
*/
|
|
11
|
+
export const normalizeVersionFormatting = (sql) => {
|
|
12
|
+
if (typeof sql !== 'string') return sql;
|
|
13
|
+
return sql
|
|
14
|
+
.replace(/\b(tinyint|smallint|mediumint|int|integer|bigint)\((\d+)\)(\s+unsigned)?(?!\s+(?:unsigned\s+)?zerofill)/gi,
|
|
15
|
+
(match, type, width, unsigned = '') => type.toLowerCase() === 'tinyint' && width === '1' && !unsigned ? match : `${type}${unsigned}`)
|
|
16
|
+
.replace(/\byear\(4\)/gi, 'year')
|
|
17
|
+
.replace(/\butf8mb3(?=\b|_)/gi, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A column type compared across MySQL versions
|
|
21
|
+
export const normalizeColumnType = (type) => normalizeVersionFormatting(type);
|
|
22
|
+
|
|
23
|
+
export const normalizeSQLDefinition = (definition) => {
|
|
24
|
+
return normalizeVersionFormatting(definition)
|
|
25
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with a single space
|
|
26
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
27
|
+
.trim(); // Trim leading and trailing whitespace
|
|
28
|
+
}
|
package/src/options.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional file in the dump/template directory:
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "definer": "root@%", // DEFINER for every procedure, view and trigger
|
|
9
|
+
* "autoIncrement": false, // default for every table
|
|
10
|
+
* "databases": {
|
|
11
|
+
* "Alerts": {
|
|
12
|
+
* "AlarmRules": {
|
|
13
|
+
* "autoIncrement": 1000, // overrides the default for this table
|
|
14
|
+
* "rowKey": ["idAlarmRule"], // columns that identify a row; defaults to the primary key
|
|
15
|
+
* "rows": [{ "idAlarmRule": 1, "name": "Default" }]
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* autoIncrement controls the table's AUTO_INCREMENT=N counter in CREATE TABLE
|
|
22
|
+
* (not the AUTO_INCREMENT column attribute):
|
|
23
|
+
* true keep the counter as dumped (the default)
|
|
24
|
+
* false remove it, so new tables start at 1
|
|
25
|
+
* N start new tables at N
|
|
26
|
+
*
|
|
27
|
+
* rows are inserted when no row with the same rowKey values exists; existing rows are never changed.
|
|
28
|
+
*
|
|
29
|
+
* definer replaces the DEFINER of every procedure, view and trigger in the
|
|
30
|
+
* template, so one whose database definer differs is reported and recreated
|
|
31
|
+
* with it. Written as root@%, `root`@`%` or 'root'@'%'.
|
|
32
|
+
*/
|
|
33
|
+
export const OPTIONS_FILE = 'syncOptions.json';
|
|
34
|
+
|
|
35
|
+
export const loadSyncOptions = async (dir) => {
|
|
36
|
+
const filePath = path.join(dir, OPTIONS_FILE);
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.code === 'ENOENT') return {};
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
let options;
|
|
45
|
+
try {
|
|
46
|
+
options = JSON.parse(content);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(`Invalid JSON in ${filePath}: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
validateSyncOptions(options, filePath);
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isValidAutoIncrement = (value) => value === undefined || typeof value === 'boolean' || (Number.isInteger(value) && value > 0);
|
|
55
|
+
|
|
56
|
+
const validateSyncOptions = (options, filePath) => {
|
|
57
|
+
if (options.definer !== undefined && formatDefiner(options.definer) === null) {
|
|
58
|
+
throw new Error(`${filePath}: definer must look like root@% or \`root\`@\`%\``);
|
|
59
|
+
}
|
|
60
|
+
if (!isValidAutoIncrement(options.autoIncrement)) {
|
|
61
|
+
throw new Error(`${filePath}: autoIncrement must be true, false or a positive integer`);
|
|
62
|
+
}
|
|
63
|
+
for (const [database, tables] of Object.entries(options.databases || {})) {
|
|
64
|
+
for (const [table, tableOptions] of Object.entries(tables || {})) {
|
|
65
|
+
const where = `${filePath}: ${database}.${table}`;
|
|
66
|
+
if (!isValidAutoIncrement(tableOptions.autoIncrement)) {
|
|
67
|
+
throw new Error(`${where}.autoIncrement must be true, false or a positive integer`);
|
|
68
|
+
}
|
|
69
|
+
if (tableOptions.rows !== undefined && (!Array.isArray(tableOptions.rows) || tableOptions.rows.some(row => !row || typeof row !== 'object' || Array.isArray(row)))) {
|
|
70
|
+
throw new Error(`${where}.rows must be an array of objects`);
|
|
71
|
+
}
|
|
72
|
+
if (tableOptions.rowKey !== undefined && (!Array.isArray(tableOptions.rowKey) || tableOptions.rowKey.length === 0)) {
|
|
73
|
+
throw new Error(`${where}.rowKey must be a non-empty array of column names`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const getTableOptions = (syncOptions, database, table) => ({
|
|
80
|
+
autoIncrement: syncOptions?.autoIncrement ?? true,
|
|
81
|
+
...syncOptions?.databases?.[database]?.[table]
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const removeAutoIncrementCounter = (createSQL) => createSQL.replace(/\s+AUTO_INCREMENT=\d+/i, '');
|
|
85
|
+
|
|
86
|
+
// Apply a table's autoIncrement option to its CREATE TABLE statement
|
|
87
|
+
export const applyAutoIncrementOption = (createSQL, autoIncrement = true) => {
|
|
88
|
+
if (!createSQL || autoIncrement === true) return createSQL;
|
|
89
|
+
const withoutCounter = removeAutoIncrementCounter(createSQL);
|
|
90
|
+
if (autoIncrement === false) return withoutCounter;
|
|
91
|
+
if (/\)\s*ENGINE=\w+/i.test(withoutCounter)) {
|
|
92
|
+
return withoutCounter.replace(/(\)\s*ENGINE=\w+)/i, `$1 AUTO_INCREMENT=${autoIncrement}`);
|
|
93
|
+
}
|
|
94
|
+
return withoutCounter.replace(/\)(\s*;?\s*)$/, `) AUTO_INCREMENT=${autoIncrement}$1`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// root@%, `root`@`%`, 'root'@'%' or DEFINER=... -> `root`@`%`; null if it isn't a user@host
|
|
98
|
+
export const formatDefiner = (definer) => {
|
|
99
|
+
if (typeof definer !== 'string') return null;
|
|
100
|
+
const match = definer.trim().replace(/^DEFINER\s*=\s*/i, '')
|
|
101
|
+
.match(/^(?:`([^`]+)`|'([^']+)'|([^@`'\s]+))@(?:`([^`]+)`|'([^']+)'|([^@`'\s]+))$/);
|
|
102
|
+
if (!match) return null;
|
|
103
|
+
const user = match[1] ?? match[2] ?? match[3];
|
|
104
|
+
const host = match[4] ?? match[5] ?? match[6];
|
|
105
|
+
return `\`${user}\`@\`${host}\``;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The DEFINER clause in a CREATE PROCEDURE/VIEW/TRIGGER header
|
|
109
|
+
const definerClauseRegex = /DEFINER\s*=\s*(?:`[^`]*`|'[^']*'|[^\s@]+)@(?:`[^`]*`|'[^']*'|[^\s]+)/i;
|
|
110
|
+
|
|
111
|
+
// Replace the DEFINER in one definition; unchanged when definer isn't set or the definition has none
|
|
112
|
+
export const applyDefiner = (definition, definer) => {
|
|
113
|
+
const formatted = definer ? formatDefiner(definer) : null;
|
|
114
|
+
if (!formatted || typeof definition !== 'string') return definition;
|
|
115
|
+
return definition.replace(definerClauseRegex, `DEFINER=${formatted}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A copy of a database structure with the definer option applied to its procedures, views and triggers
|
|
119
|
+
export const applyDefinerOption = (structure, definer) => {
|
|
120
|
+
if (!definer || !structure) return structure;
|
|
121
|
+
const tables = Object.fromEntries(Object.entries(structure.tables || {}).map(([name, table]) => [name, {
|
|
122
|
+
...table,
|
|
123
|
+
triggers: (table.triggers || []).map(trigger => ({ ...trigger, Definition: applyDefiner(trigger.Definition, definer) }))
|
|
124
|
+
}]));
|
|
125
|
+
const result = { ...structure, tables };
|
|
126
|
+
if (structure.procedures) {
|
|
127
|
+
result.procedures = structure.procedures.map(procedure => ({ ...procedure, Definition: applyDefiner(procedure.Definition, definer) }));
|
|
128
|
+
}
|
|
129
|
+
if (structure.views) {
|
|
130
|
+
result.views = Object.fromEntries(Object.entries(structure.views).map(([name, view]) => [name, applyDefiner(view, definer)]));
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
package/src/parsers.js
CHANGED
|
@@ -11,9 +11,6 @@ export const parseCreateTableSQL = (createTableSQL) => {
|
|
|
11
11
|
// Split the SQL statement into lines and filter out empty lines
|
|
12
12
|
const lines = createTableSQL.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
13
13
|
|
|
14
|
-
// Regular expression to match column definitions
|
|
15
|
-
const columnRegex = /`([^`]+)`\s+([^\s,]+)(\s+NOT NULL|\s+NULL)?(\s+DEFAULT\s+([^,\s]+))?(\s+AUTO_INCREMENT)?/;
|
|
16
|
-
|
|
17
14
|
// Regular expression to match index definitions
|
|
18
15
|
const indexRegex_Old = /(PRIMARY|UNIQUE)?\s*KEY\s*`([^`]+)`\s*\(([^)]+)\)|PRIMARY KEY\s*\(([^)]+)\)/;
|
|
19
16
|
|
|
@@ -30,35 +27,12 @@ export const parseCreateTableSQL = (createTableSQL) => {
|
|
|
30
27
|
if (line.includes('CREATE TABLE')) {
|
|
31
28
|
continue;
|
|
32
29
|
}
|
|
33
|
-
//
|
|
34
|
-
const
|
|
30
|
+
// Column definitions are the only lines that start with a backticked name
|
|
31
|
+
const column = parseColumnDefinition(line);
|
|
35
32
|
const indexMatch = indexRegex.exec(line);
|
|
36
|
-
const indexMatch_Old = indexRegex_Old.exec(line);
|
|
37
33
|
|
|
38
34
|
const constraintMatch = parseConstraint(line);
|
|
39
|
-
if (
|
|
40
|
-
const columnType = parseColumnType(line);
|
|
41
|
-
const column = {
|
|
42
|
-
Field: columnMatch[1],
|
|
43
|
-
Type: columnType
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Set Null property based on NOT NULL presence
|
|
47
|
-
if (columnMatch[3] && columnMatch[3].includes('NOT NULL')) {
|
|
48
|
-
column.NotNull = true; // Set to false if NOT NULL is present
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Set Default property if it exists
|
|
52
|
-
if (columnMatch[5]) {
|
|
53
|
-
// console.log('columnMatch[5]', columnMatch[5], columnMatch[5].replace(/'/g, ''))
|
|
54
|
-
column.Default = columnMatch[5].replace(/'/g, '');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Set AutoIncrement property if it exists
|
|
58
|
-
if (columnMatch[6]) {
|
|
59
|
-
column.AutoIncrement = true; // Set to true if AUTO_INCREMENT is present
|
|
60
|
-
}
|
|
61
|
-
|
|
35
|
+
if (column) {
|
|
62
36
|
columns.push(column);
|
|
63
37
|
} else if (indexMatch && !constraintMatch) {
|
|
64
38
|
|
|
@@ -141,11 +115,83 @@ export const parseConstraint = (sql) => {
|
|
|
141
115
|
};
|
|
142
116
|
};
|
|
143
117
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
118
|
+
// A quoted SQL string, allowing '' and backslash escapes inside it
|
|
119
|
+
const quotedStringRegex = /'(?:[^'\\]|\\.|'')*'/g;
|
|
120
|
+
|
|
121
|
+
const unquote = (quoted) => quoted.slice(1, -1).replace(/''/g, "'").replace(/\\(.)/g, '$1');
|
|
122
|
+
|
|
123
|
+
// Type with its parameters: int, float(10,2), enum('a','b'), varchar(45); then unsigned/zerofill
|
|
124
|
+
const columnTypeRegex = /^([a-zA-Z]+(?:\((?:'(?:[^'\\]|\\.|'')*'|[^)'])*\))?)((?:\s+(?:unsigned|zerofill)\b)*)/i;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse one column line of a SHOW CREATE TABLE statement, e.g.
|
|
128
|
+
* `IID` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'x',
|
|
129
|
+
* Returns null if the line isn't a column definition.
|
|
130
|
+
*/
|
|
131
|
+
export const parseColumnDefinition = (line) => {
|
|
132
|
+
const lineMatch = line.trim().match(/^`([^`]+)`\s+(.*?),?$/);
|
|
133
|
+
if (!lineMatch) return null;
|
|
134
|
+
const typeMatch = lineMatch[2].match(columnTypeRegex);
|
|
135
|
+
if (!typeMatch) return null;
|
|
136
|
+
|
|
137
|
+
const column = {
|
|
138
|
+
Field: lineMatch[1],
|
|
139
|
+
// unsigned/zerofill are part of the type: a column restated without them changes
|
|
140
|
+
Type: typeMatch[1] + typeMatch[2].toLowerCase()
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Swap quoted strings for numbered placeholders so keywords inside a
|
|
144
|
+
// COMMENT or DEFAULT value aren't matched as attributes
|
|
145
|
+
const strings = [];
|
|
146
|
+
const quoted = [];
|
|
147
|
+
const rest = lineMatch[2].slice(typeMatch[0].length).replace(quotedStringRegex, (text) => {
|
|
148
|
+
strings.push(unquote(text));
|
|
149
|
+
quoted.push(text);
|
|
150
|
+
return `'${strings.length - 1}'`;
|
|
151
|
+
});
|
|
152
|
+
// Put the original quoted text back, e.g. b'0' or (_utf8mb4'[]')
|
|
153
|
+
const restoreQuoted = (text) => text.replace(/'(\d+)'/g, (_, i) => quoted[i]);
|
|
154
|
+
|
|
155
|
+
if (/\bNOT NULL\b/i.test(rest)) {
|
|
156
|
+
column.NotNull = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+('\d+'|\((?:[^()]|\([^()]*\))*\)|[^\s,]+)/i);
|
|
160
|
+
if (defaultMatch) {
|
|
161
|
+
// A plain string default is stored unquoted; anything else (NULL,
|
|
162
|
+
// CURRENT_TIMESTAMP, b'0', an expression) is kept as written
|
|
163
|
+
const whole = defaultMatch[1].match(/^'(\d+)'$/);
|
|
164
|
+
column.Default = whole ? strings[whole[1]] : restoreQuoted(defaultMatch[1]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const generatedMatch = rest.match(/\bGENERATED ALWAYS AS\s*(\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\))\s*(STORED|VIRTUAL)?/i);
|
|
168
|
+
if (generatedMatch) {
|
|
169
|
+
column.Generated = `${restoreQuoted(generatedMatch[1])}${generatedMatch[2] ? ` ${generatedMatch[2].toUpperCase()}` : ''}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (/\bAUTO_INCREMENT\b/i.test(rest)) {
|
|
173
|
+
column.AutoIncrement = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const onUpdateMatch = rest.match(/\bON UPDATE\s+(CURRENT_TIMESTAMP(?:\(\d*\))?)/i);
|
|
177
|
+
if (onUpdateMatch) {
|
|
178
|
+
column.OnUpdate = onUpdateMatch[1];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const charsetMatch = rest.match(/\bCHARACTER SET\s+(\w+)/i);
|
|
182
|
+
if (charsetMatch) {
|
|
183
|
+
column.Charset = charsetMatch[1];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const collateMatch = rest.match(/\bCOLLATE\s+(\w+)/i);
|
|
187
|
+
if (collateMatch) {
|
|
188
|
+
column.Collate = collateMatch[1];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const commentMatch = rest.match(/\bCOMMENT\s+'(\d+)'/i);
|
|
192
|
+
if (commentMatch) {
|
|
193
|
+
column.Comment = strings[commentMatch[1]];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return column;
|
|
151
197
|
};
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
// The mysqlsyncr package folder, from src/ or the dist/ build. The CJS build
|
|
6
|
+
// rewrites import.meta.url from __filename; a global __dirname can't be trusted
|
|
7
|
+
// (node -e sets it to '.')
|
|
8
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
|
|
10
|
+
export const isInsidePackage = (dir) => {
|
|
11
|
+
const relative = path.relative(packageRoot, path.resolve(dir));
|
|
12
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Throw if dir is the mysqlsyncr package folder or inside it. Dumps and
|
|
17
|
+
* templates kept there are lost when the package is reinstalled or updated.
|
|
18
|
+
*/
|
|
19
|
+
export const assertOutsidePackage = (dir) => {
|
|
20
|
+
if (dir && isInsidePackage(dir)) {
|
|
21
|
+
throw new Error(`${path.resolve(dir)} is inside the mysqlsyncr package folder (${packageRoot}). Choose a folder outside it with --output.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why dir can't be read as a template: it doesn't exist, isn't a folder, or has
|
|
27
|
+
* no <database>/<database>.json in it. Null when it's usable.
|
|
28
|
+
*/
|
|
29
|
+
export const templateFolderProblem = (dir) => {
|
|
30
|
+
const resolved = path.resolve(dir);
|
|
31
|
+
let stat;
|
|
32
|
+
try {
|
|
33
|
+
stat = fs.statSync(resolved);
|
|
34
|
+
} catch {
|
|
35
|
+
return `${resolved} doesn't exist.`;
|
|
36
|
+
}
|
|
37
|
+
if (!stat.isDirectory()) return `${resolved} isn't a folder.`;
|
|
38
|
+
const hasDatabase = fs.readdirSync(resolved, { withFileTypes: true })
|
|
39
|
+
.some(entry => entry.isDirectory() && fs.existsSync(path.join(resolved, entry.name, `${entry.name}.json`)));
|
|
40
|
+
return hasDatabase ? null : `${resolved} has no template in it (no <database>/<database>.json). Run dump first, or choose the template folder.`;
|
|
41
|
+
}
|