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/src/create.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const generateCreateTableSQL = (tableName, fields) => {
|
|
2
|
+
const columns = fields
|
|
3
|
+
.map(col => {
|
|
4
|
+
const nullClause = col.Null === 'NO' ? 'NOT NULL' : 'NULL';
|
|
5
|
+
const defaultClause = col.Default === 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP' : (col.Default !== null ? `DEFAULT '${col.Default}'` : '');
|
|
6
|
+
const extraClause = col.Extra || '';
|
|
7
|
+
return `\`${col.Field}\` ${col.Type} ${nullClause} ${defaultClause} ${extraClause}`.trim();
|
|
8
|
+
})
|
|
9
|
+
.join(', ');
|
|
10
|
+
|
|
11
|
+
const primaryKey = fields.find(field => field.Key === 'PRI');
|
|
12
|
+
const primaryKeyClause = primaryKey ? `, PRIMARY KEY (\`${primaryKey.Field}\`)` : '';
|
|
13
|
+
|
|
14
|
+
return `CREATE TABLE \`${tableName}\` (${columns}${primaryKeyClause});`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { logger } from './loggers.js'; // Import the logger
|
|
2
|
+
import { normalizeSQLDefinition } from './normalizers.js';
|
|
3
|
+
export const findDifferences = (expected, current) => {
|
|
4
|
+
const differences = [];
|
|
5
|
+
|
|
6
|
+
// Detect extra tables
|
|
7
|
+
if (!current.tables) throw new Error('No tables found in current');
|
|
8
|
+
for (const tableName of Object.keys(current.tables)) {
|
|
9
|
+
if (!expected.tables[tableName]) {
|
|
10
|
+
differences.push({ type: 'extra_table', tableName });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Compare tables and fields
|
|
15
|
+
if (!expected.tables) throw new Error('No tables found in expected');
|
|
16
|
+
for (const [tableName, contents] of Object.entries(expected.tables)) {
|
|
17
|
+
//check if craete table matches from json, if so, we dont need to compare indexes, triggers, or fields
|
|
18
|
+
|
|
19
|
+
if (!current.tables[tableName]) {
|
|
20
|
+
console.log('missing_table', contents);
|
|
21
|
+
differences.push({ type: 'missing_table', tableName, createSQL: contents.createSQL });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const createTableSql = current.tables[tableName].createSQL;
|
|
26
|
+
if (createTableSql === contents.createSQL) {
|
|
27
|
+
logger(`Table ${tableName} createSQL matches from json, skipping indexes, triggers, and fields`);
|
|
28
|
+
continue;
|
|
29
|
+
} else {
|
|
30
|
+
logger(`Table ${tableName} createSQL does not match from json, comparing indexes, triggers, and fields`);
|
|
31
|
+
logger(createTableSql);
|
|
32
|
+
logger(contents.createSQL);
|
|
33
|
+
}
|
|
34
|
+
const { columns, indexes, triggers, createSQL, name, engine, charset, collate } = contents;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const currentFieldNames = current.tables[tableName].columns.map(f => f.Field);
|
|
38
|
+
const expectedFieldNames = columns.map(f => f.Field);
|
|
39
|
+
|
|
40
|
+
logger('Detecting missing fields for table', tableName);
|
|
41
|
+
// Detect missing fields
|
|
42
|
+
for (const field of columns) {
|
|
43
|
+
if (!currentFieldNames.includes(field.Field)) {
|
|
44
|
+
differences.push({ type: 'missing_field', tableName, field });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logger('Detecting extra fields for table', tableName);
|
|
49
|
+
// Detect extra fields
|
|
50
|
+
for (const field of current.tables[tableName].columns) {
|
|
51
|
+
if (!expectedFieldNames.includes(field.Field)) {
|
|
52
|
+
differences.push({ type: 'extra_field', tableName, field });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// verify Type Null and Default
|
|
58
|
+
logger('Verifying Type Null and Default for table', tableName);
|
|
59
|
+
for (const field of columns) {
|
|
60
|
+
const currentField = current.tables[tableName].columns.find(f => f.Field === field.Field);
|
|
61
|
+
if (field.Type !== currentField.Type) {
|
|
62
|
+
differences.push({ type: 'mismatched_field', tableName, field });
|
|
63
|
+
}
|
|
64
|
+
if (field.Null !== currentField.Null) {
|
|
65
|
+
differences.push({ type: 'mismatched_field', tableName, field });
|
|
66
|
+
}
|
|
67
|
+
if (field.Default !== currentField.Default) {
|
|
68
|
+
differences.push({ type: 'mismatched_field', tableName, field });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// verify indexes
|
|
73
|
+
logger('Verifying indexes for table', tableName);
|
|
74
|
+
for (const index of indexes) {
|
|
75
|
+
// Check if the current index matches the expected index structure
|
|
76
|
+
const currentIndex = current.tables[tableName].indexes.find(i =>
|
|
77
|
+
i.ColumnName.sort().join(', ') === index.ColumnName.sort().join(', ')
|
|
78
|
+
);
|
|
79
|
+
if (!currentIndex) {
|
|
80
|
+
logger('Missing index', index);
|
|
81
|
+
differences.push({ type: 'missing_index', tableName, indexName: index.Name, index });
|
|
82
|
+
} else {
|
|
83
|
+
// If needed, you can add further checks for uniqueness or other properties here
|
|
84
|
+
if (currentIndex.Name == index.Name) {
|
|
85
|
+
if (currentIndex.Unique !== index.Unique) {
|
|
86
|
+
differences.push({ type: 'mismatched_index', from: 'unique', tableName, indexName: index.Name, index });
|
|
87
|
+
} else if (currentIndex.Primary !== index.Primary) {
|
|
88
|
+
differences.push({ type: 'mismatched_index', from: 'primary', tableName, indexName: index.Name, index });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const index of current.tables[tableName].indexes) {
|
|
95
|
+
logger('Checking index', index);
|
|
96
|
+
let indexFound = false
|
|
97
|
+
|
|
98
|
+
// Sort the ColumnName array for proper comparison
|
|
99
|
+
const sortedCurrentIndexColumns = index.ColumnName.sort().join(', ');
|
|
100
|
+
if (!indexes.some(i => i.ColumnName.sort().join(', ') === sortedCurrentIndexColumns)) {
|
|
101
|
+
differences.push({ type: 'extra_index', tableName, indexName: index.Name, index });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//verify triggers
|
|
106
|
+
logger('Verifying triggers for table', tableName);
|
|
107
|
+
for (const trigger of triggers) {
|
|
108
|
+
const currentTrigger = current.tables[tableName].triggers.find(t => t.Name === trigger.Name);
|
|
109
|
+
if (!currentTrigger) {
|
|
110
|
+
logger('Missing trigger', trigger);
|
|
111
|
+
differences.push({ type: 'missing_trigger', tableName, trigger });
|
|
112
|
+
} else {
|
|
113
|
+
// Compare additional properties if needed
|
|
114
|
+
if (currentTrigger.Event !== trigger.Event || currentTrigger.Statement !== trigger.Statement) {
|
|
115
|
+
logger('Mismatched trigger', { expected: trigger, current: currentTrigger });
|
|
116
|
+
differences.push({ type: 'mismatched_trigger', tableName, trigger });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const trigger of current.tables[tableName].triggers) {
|
|
121
|
+
if (!triggers.some(t => t.Name === trigger.Name)) {
|
|
122
|
+
logger('Extra trigger', trigger);
|
|
123
|
+
differences.push({ type: 'extra_trigger', tableName, trigger });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// Compare stored procedures
|
|
132
|
+
if (!expected.procedures) throw new Error('No procedures found in expected');
|
|
133
|
+
for (const [procName, contents] of Object.entries(expected.procedures)) {
|
|
134
|
+
if (!current.procedures[procName]) {
|
|
135
|
+
console.log('missing_procedure', contents);
|
|
136
|
+
differences.push({ type: 'missing_procedure', ...contents });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const currentProc = current.procedures[procName];
|
|
141
|
+
|
|
142
|
+
// Check if the procedure definition matches
|
|
143
|
+
if (currentProc.Definition !== contents.Definition) {
|
|
144
|
+
const normalizedCurrentDef = normalizeSQLDefinition(currentProc.Definition);
|
|
145
|
+
const normalizedNewDef = normalizeSQLDefinition(contents.Definition);
|
|
146
|
+
|
|
147
|
+
if (normalizedCurrentDef !== normalizedNewDef) {
|
|
148
|
+
differences.push({ type: 'mismatched_procedure', ...contents });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Compare views
|
|
156
|
+
if (!expected.views) throw new Error('No views found in expected');
|
|
157
|
+
for (const [viewName, expectedDefinition] of Object.entries(expected.views)) {
|
|
158
|
+
if (!current.views[viewName]) {
|
|
159
|
+
differences.push({ type: 'missing_view', viewName, definition: expectedDefinition });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const currentDefinition = current.views[viewName];
|
|
164
|
+
if (currentDefinition !== expectedDefinition) {
|
|
165
|
+
differences.push({ type: 'mismatched_view', viewName, definition: expectedDefinition, current: currentDefinition });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Detect extra views
|
|
170
|
+
if (!current.views) throw new Error('No views found in current');
|
|
171
|
+
for (const viewName of Object.keys(current.views)) {
|
|
172
|
+
if (!expected.views[viewName]) {
|
|
173
|
+
differences.push({ type: 'extra_view', viewName });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return differences;
|
|
178
|
+
}
|
package/src/dump.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getDatabases, getDatabaseStructure } from './getters.js';
|
|
4
|
+
|
|
5
|
+
export const dumpAllDatabases = async (connection, command, options) => {
|
|
6
|
+
const databases = await getDatabases(connection);
|
|
7
|
+
for (const database of databases) {
|
|
8
|
+
let dbStructure = await getDatabaseStructure(connection, database, command, options);
|
|
9
|
+
await fs.writeFile(path.join(options.output, database, `${database}.json`), JSON.stringify(dbStructure, null, 4), 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
package/src/getters.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { parseCreateTableSQL } from './parsers.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { logger } from './loggers.js';
|
|
5
|
+
|
|
6
|
+
export const getDatabases = async (connection) => {
|
|
7
|
+
const databases = await connection.query('SHOW DATABASES');
|
|
8
|
+
return databases
|
|
9
|
+
.map(db => db.Database)
|
|
10
|
+
.filter(db => !['information_schema', 'mysql', 'performance_schema', 'sys'].includes(db));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export const getDatabaseStructure = async (connection, database, command, options) => {
|
|
15
|
+
if (connection.state === 'disconnected') {
|
|
16
|
+
await connection.reconnect(); // Ensure the connection is open
|
|
17
|
+
}
|
|
18
|
+
await connection.query(`USE \`${database}\``); // Switch to the target database
|
|
19
|
+
const tables = {};
|
|
20
|
+
const procedures = [];
|
|
21
|
+
const views = {};
|
|
22
|
+
const dbDir = path.join(options.output, database); // Use argv.output for the base path
|
|
23
|
+
|
|
24
|
+
// Create the output directory for the database if it doesn't exist
|
|
25
|
+
if (command === 'dump') {
|
|
26
|
+
const tableDir = path.join(dbDir, 'tables'); // Subfolder for table SQL files
|
|
27
|
+
await fs.mkdir(tableDir, { recursive: true }); // Create the tables directory
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get tables and their structures
|
|
31
|
+
const tableRows = await connection.query('SHOW FULL TABLES WHERE Table_Type != "VIEW"');
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for (const row of tableRows) {
|
|
35
|
+
const tableName = Object.values(row)[0]; // Get the first value in the row object
|
|
36
|
+
if (!tableName) {
|
|
37
|
+
logger(`Undefined table name found in database ${database}`, row);
|
|
38
|
+
continue; // Skip undefined table names
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get the CREATE TABLE statement
|
|
42
|
+
const createTableRow = await connection.query(`SHOW CREATE TABLE \`${tableName}\``);
|
|
43
|
+
const createTableSQL = createTableRow[0]['Create Table'];
|
|
44
|
+
|
|
45
|
+
const parsedTable = parseCreateTableSQL(createTableSQL);
|
|
46
|
+
|
|
47
|
+
// Structure the table object
|
|
48
|
+
tables[tableName] = {
|
|
49
|
+
columns: parsedTable.columns, // Array of columns
|
|
50
|
+
indexes: parsedTable.indexes, // Array of indexes
|
|
51
|
+
constraints: parsedTable.constraints, // Array of constraints
|
|
52
|
+
triggers: [], // Initialize triggers array
|
|
53
|
+
createSQL: createTableSQL // Store the CREATE TABLE SQL if needed
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Save the CREATE TABLE statement to an individual SQL file for each table
|
|
57
|
+
if (command === 'dump') {
|
|
58
|
+
const tableDir = path.join(dbDir, 'tables'); // Subfolder for table SQL files
|
|
59
|
+
const filePath = path.join(tableDir, `${tableName}.sql`); // Use the table directory for file saving
|
|
60
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true }); // Create the tables directory if it doesn't exist
|
|
61
|
+
await fs.writeFile(filePath, `${createTableSQL}\n`, 'utf-8');
|
|
62
|
+
logger(`Table structure saved to: ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get stored procedures
|
|
67
|
+
const routineRows = await connection.query(`SHOW PROCEDURE STATUS WHERE Db = ?`, [database]);
|
|
68
|
+
|
|
69
|
+
for (const row of routineRows) {
|
|
70
|
+
let createSQLrow = await connection.query(`SHOW CREATE PROCEDURE \`${row.Name}\``);
|
|
71
|
+
let createSQL = createSQLrow[0]['Create Procedure'];
|
|
72
|
+
procedures.push({
|
|
73
|
+
Name: row.Name,
|
|
74
|
+
Definition: createSQL
|
|
75
|
+
}); // Add procedure names to the array
|
|
76
|
+
if (command === 'dump') {
|
|
77
|
+
const filePath = path.join(dbDir, 'procedures', `${row.Name}.sql`); // Use the table directory for file saving
|
|
78
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true }); // Create the procedures directory if it doesn't exist
|
|
79
|
+
await fs.writeFile(filePath, `${createSQL}\n`, 'utf-8');
|
|
80
|
+
logger(`Procedure definition saved to: ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get triggers
|
|
85
|
+
const triggerRows = await connection.query(`
|
|
86
|
+
SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE
|
|
87
|
+
FROM INFORMATION_SCHEMA.TRIGGERS
|
|
88
|
+
WHERE TRIGGER_SCHEMA = ?
|
|
89
|
+
`, [database]);
|
|
90
|
+
|
|
91
|
+
for (const row of triggerRows) {
|
|
92
|
+
const triggerName = row.TRIGGER_NAME;
|
|
93
|
+
const tableName = row.EVENT_OBJECT_TABLE;
|
|
94
|
+
|
|
95
|
+
// Ensure triggerDefinition is defined before using it
|
|
96
|
+
const triggerDefinitionRow = await connection.query(`SHOW CREATE TRIGGER \`${triggerName}\``);
|
|
97
|
+
const triggerDefinition = triggerDefinitionRow[0]['SQL Original Statement'];
|
|
98
|
+
|
|
99
|
+
if (triggerDefinition) {
|
|
100
|
+
|
|
101
|
+
tables[tableName].triggers.push({
|
|
102
|
+
Name: triggerName,
|
|
103
|
+
Definition: triggerDefinition
|
|
104
|
+
});
|
|
105
|
+
if (command === 'dump') {
|
|
106
|
+
const tableDir = path.join(dbDir, 'tables'); // Subfolder for table SQL files
|
|
107
|
+
const triggerFilePath = path.join(tableDir, 'triggers', `${triggerName}.sql`);
|
|
108
|
+
await fs.mkdir(path.dirname(triggerFilePath), { recursive: true }); // Create the triggers directory if it doesn't exist
|
|
109
|
+
await fs.writeFile(triggerFilePath, `${triggerDefinition}\n`, 'utf-8');
|
|
110
|
+
logger(`Trigger definition saved to: ${triggerFilePath}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
} else {
|
|
114
|
+
console.warn(`Trigger ${triggerName} has no action statement.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get views
|
|
119
|
+
const viewRows = await connection.query(`SHOW FULL TABLES IN \`${database}\` WHERE TABLE_TYPE LIKE 'VIEW'`);
|
|
120
|
+
for (const row of viewRows) {
|
|
121
|
+
let viewName = row[`Tables_in_${database.toLowerCase()}`];
|
|
122
|
+
if (!viewName) viewName = row[`Tables_in_${database}`];
|
|
123
|
+
logger(`getting view Tables_in_${database}`, viewName);
|
|
124
|
+
try {
|
|
125
|
+
const viewDefinition = await connection.query(`SHOW CREATE VIEW \`${viewName}\``);
|
|
126
|
+
views[viewName] = viewDefinition[0]['Create View'];
|
|
127
|
+
if (command === 'dump') {
|
|
128
|
+
const filePath = path.join(dbDir, 'views', `${viewName}.sql`); // Use the table directory for file saving
|
|
129
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true }); // Create the views directory if it doesn't exist
|
|
130
|
+
await fs.writeFile(filePath, `${viewDefinition[0]['Create View']}\n`, 'utf-8');
|
|
131
|
+
logger(`View definition saved to: ${filePath}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger(`Error retrieving definition for view ${viewName}:`, error.message);
|
|
135
|
+
logger(row)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { tables, procedures, views };
|
|
140
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
//TODO
|
|
4
|
+
|
|
5
|
+
//1. Check constraints in tables
|
|
6
|
+
//2. fix index recognition
|
|
7
|
+
|
|
8
|
+
import mysql from 'mysql2';
|
|
9
|
+
import util from 'util';
|
|
10
|
+
|
|
11
|
+
import { cli } from './cli.js';
|
|
12
|
+
import { dumpAllDatabases } from './dump.js';
|
|
13
|
+
import { compareAllDatabases } from './compare.js';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DBStructureChecker {
|
|
17
|
+
constructor(config, targetDatabase = null) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.targetDatabase = targetDatabase;
|
|
20
|
+
this.connection = mysql.createConnection(this.config);
|
|
21
|
+
this.connection.on('error', (err) => {
|
|
22
|
+
console.error('Database connection error:', err);
|
|
23
|
+
});
|
|
24
|
+
this.connection.query = util.promisify(this.connection.query); // Promisify for async/await
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async reconnect() {
|
|
28
|
+
this.connection = mysql.createConnection(this.config);
|
|
29
|
+
this.connection.query = util.promisify(this.connection.query); // Promisify for async/await
|
|
30
|
+
await this.connection.connect(); // Connect to the database
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async closeConnection() {
|
|
34
|
+
if (this.connection.state !== 'disconnected') {
|
|
35
|
+
await this.connection.end();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// CLI Setup
|
|
43
|
+
(async () => {
|
|
44
|
+
const { command, config, argv } = cli();
|
|
45
|
+
|
|
46
|
+
const checker = new DBStructureChecker(config, argv.database);
|
|
47
|
+
checker.connection.on('connect', async () => {
|
|
48
|
+
console.log('Connected to the database');
|
|
49
|
+
checker.targetDatabase = argv.database;
|
|
50
|
+
checker.command = command;
|
|
51
|
+
checker.options = argv;
|
|
52
|
+
try {
|
|
53
|
+
if (command === 'dump') {
|
|
54
|
+
console.log('Dumping the database structure to files');
|
|
55
|
+
await dumpAllDatabases(checker.connection, command, argv);
|
|
56
|
+
} else if (command === 'compare') {
|
|
57
|
+
console.log('Comparing the database structure with JSON dumps (dry run)');
|
|
58
|
+
let diffs = await compareAllDatabases(checker.connection, argv.output, false, command, argv); // Dry run
|
|
59
|
+
let totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
|
|
60
|
+
console.log('Total differences count:', totalDiffs);
|
|
61
|
+
} else if (command === 'update') {
|
|
62
|
+
console.log('Updating the database structure based on JSON dumps');
|
|
63
|
+
let totalDiffs = Infinity; // Initialize with a large number
|
|
64
|
+
let previousDiffs = totalDiffs; // Store previous differences
|
|
65
|
+
let runCount = 0; // Counter for the number of runs
|
|
66
|
+
let maxRuns = 6;
|
|
67
|
+
while (runCount < maxRuns && totalDiffs > 0 && (totalDiffs < previousDiffs || previousDiffs === Infinity)) {
|
|
68
|
+
console.log('Running update command. Run count:', runCount);
|
|
69
|
+
let diffs = await compareAllDatabases(checker.connection, argv.output, true, command, argv); // Apply updates
|
|
70
|
+
previousDiffs = totalDiffs; // Update previous differences
|
|
71
|
+
totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
|
|
72
|
+
console.log('Total differences count:', totalDiffs);
|
|
73
|
+
runCount++;
|
|
74
|
+
if(totalDiffs > 0 && runCount < maxRuns){
|
|
75
|
+
console.log('Total differences count is still greater than 0. Waiting 1 second before next run. Run count:', runCount);
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(err.message);
|
|
82
|
+
} finally {
|
|
83
|
+
await checker.closeConnection();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
})();
|
package/src/loggers.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
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 'missing_table':
|
|
39
|
+
return {
|
|
40
|
+
Type: diff.type,
|
|
41
|
+
Name: diff.tableName,
|
|
42
|
+
Details: `Table ${diff.tableName} is missing`,
|
|
43
|
+
Action: 'Table is missing',
|
|
44
|
+
};
|
|
45
|
+
case 'extra_field':
|
|
46
|
+
return {
|
|
47
|
+
Type: diff.type,
|
|
48
|
+
Name: diff.tableName,
|
|
49
|
+
Details: `Field ${diff.field.Field} is extra`,
|
|
50
|
+
Action: 'Field is extra',
|
|
51
|
+
};
|
|
52
|
+
case 'extra_table':
|
|
53
|
+
return {
|
|
54
|
+
Type: diff.type,
|
|
55
|
+
Name: diff.tableName,
|
|
56
|
+
Details: `Table ${diff.tableName} is extra`,
|
|
57
|
+
Action: 'Table is extra',
|
|
58
|
+
};
|
|
59
|
+
case 'missing_view':
|
|
60
|
+
return {
|
|
61
|
+
Type: diff.type,
|
|
62
|
+
Name: diff.viewName,
|
|
63
|
+
Details: `View ${diff.viewName} is missing`,
|
|
64
|
+
Action: 'View is missing',
|
|
65
|
+
};
|
|
66
|
+
case 'extra_index':
|
|
67
|
+
return {
|
|
68
|
+
Type: diff.type,
|
|
69
|
+
Name: diff.tableName,
|
|
70
|
+
Details: `Index ${diff.index.Name} is extra`,
|
|
71
|
+
Action: 'Index is extra',
|
|
72
|
+
};
|
|
73
|
+
case 'missing_index':
|
|
74
|
+
return {
|
|
75
|
+
Type: diff.type,
|
|
76
|
+
TableName: diff.tableName,
|
|
77
|
+
Name: diff.index.Name,
|
|
78
|
+
Details: `Index ${diff.index.Name} is missing`,
|
|
79
|
+
Action: 'Index is missing',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
case 'mismatched_index':
|
|
83
|
+
return {
|
|
84
|
+
Type: diff.type,
|
|
85
|
+
TableName: diff.tableName,
|
|
86
|
+
Name: diff.index.Name,
|
|
87
|
+
From: diff.from,
|
|
88
|
+
Details: `Index ${diff.index.Name} is mismatched`,
|
|
89
|
+
Action: 'Index is mismatched',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'extra_trigger':
|
|
93
|
+
return {
|
|
94
|
+
Type: diff.type,
|
|
95
|
+
Name: diff.trigger.Name,
|
|
96
|
+
Details: `Trigger ${diff.trigger.Name} is extra`,
|
|
97
|
+
Action: 'Trigger is extra',
|
|
98
|
+
};
|
|
99
|
+
case 'mismatched_trigger':
|
|
100
|
+
return {
|
|
101
|
+
Type: diff.type,
|
|
102
|
+
Name: diff.trigger.Name,
|
|
103
|
+
Details: `Trigger ${diff.trigger.Name} is mismatched`,
|
|
104
|
+
Action: 'Trigger is mismatched',
|
|
105
|
+
};
|
|
106
|
+
case 'missing_trigger':
|
|
107
|
+
console.log('missing_trigger', diff)
|
|
108
|
+
return {
|
|
109
|
+
Type: diff.type,
|
|
110
|
+
Name: diff.trigger.Name,
|
|
111
|
+
Details: `Trigger ${diff.trigger.Name} is missing`,
|
|
112
|
+
Action: 'Trigger is missing',
|
|
113
|
+
};
|
|
114
|
+
default:
|
|
115
|
+
return {
|
|
116
|
+
Type: diff.type,
|
|
117
|
+
Details: `Unhandled difference type: ${diff.type}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Filter out any undefined or null entries
|
|
123
|
+
const filteredDifferences = formattedDifferences.filter(diff => diff);
|
|
124
|
+
|
|
125
|
+
// Log the differences in a table format
|
|
126
|
+
if (filteredDifferences.length > 0) {
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
logger(`Dry Run: Differences for database ${database}:`);
|
|
129
|
+
} else {
|
|
130
|
+
logger(`Differences applied for database ${database}:`);
|
|
131
|
+
}
|
|
132
|
+
console.table(filteredDifferences);
|
|
133
|
+
} else {
|
|
134
|
+
if (dryRun) {
|
|
135
|
+
logger(`Dry Run: No differences found. Database: ${database}`);
|
|
136
|
+
} else {
|
|
137
|
+
logger(`No differences found. Database: ${database}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|