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/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
+ }
@@ -0,0 +1,6 @@
1
+ export const normalizeSQLDefinition = (definition) => {
2
+ return definition
3
+ .replace(/\s+/g, ' ') // Replace multiple whitespace with a single space
4
+ .replace(/'/g, "\\'") // Escape single quotes
5
+ .trim(); // Trim leading and trailing whitespace
6
+ }