mcp-migration-advisor 0.2.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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Flyway SQL migration parser.
3
+ *
4
+ * Parses Flyway V__*.sql and R__*.sql migration files.
5
+ * Extracts DDL statements and categorizes operations.
6
+ */
7
+ const FLYWAY_VERSIONED_RE = /^V(\d+(?:[._]\d+)*)__(.+)\.sql$/i;
8
+ const FLYWAY_REPEATABLE_RE = /^R__(.+)\.sql$/i;
9
+ /**
10
+ * Parse a Flyway migration filename to extract version and description.
11
+ */
12
+ export function parseFlywayFilename(filename) {
13
+ const basename = filename.replace(/^.*[/\\]/, "");
14
+ const vMatch = basename.match(FLYWAY_VERSIONED_RE);
15
+ if (vMatch) {
16
+ return {
17
+ version: vMatch[1].replace(/_/g, "."),
18
+ description: vMatch[2].replace(/_/g, " "),
19
+ isRepeatable: false,
20
+ };
21
+ }
22
+ const rMatch = basename.match(FLYWAY_REPEATABLE_RE);
23
+ if (rMatch) {
24
+ return {
25
+ version: null,
26
+ description: rMatch[1].replace(/_/g, " "),
27
+ isRepeatable: true,
28
+ };
29
+ }
30
+ return { version: null, description: basename, isRepeatable: false };
31
+ }
32
+ /**
33
+ * Split SQL text into individual statements.
34
+ * Handles semicolons, ignoring those inside strings and comments.
35
+ */
36
+ function splitStatements(sql) {
37
+ // Remove block comments
38
+ let cleaned = sql.replace(/\/\*[\s\S]*?\*\//g, "");
39
+ // Remove line comments
40
+ cleaned = cleaned.replace(/--.*$/gm, "");
41
+ const stmts = [];
42
+ let current = "";
43
+ for (const char of cleaned) {
44
+ if (char === ";") {
45
+ const trimmed = current.trim();
46
+ if (trimmed.length > 0) {
47
+ stmts.push(trimmed);
48
+ }
49
+ current = "";
50
+ }
51
+ else {
52
+ current += char;
53
+ }
54
+ }
55
+ const last = current.trim();
56
+ if (last.length > 0) {
57
+ stmts.push(last);
58
+ }
59
+ return stmts;
60
+ }
61
+ // Pattern matchers for DDL statement types
62
+ const CREATE_TABLE_RE = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
63
+ const ALTER_TABLE_RE = /ALTER\s+TABLE\s+(?:ONLY\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
64
+ const DROP_TABLE_RE = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
65
+ const CREATE_INDEX_RE = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|\s).*?\bON\s+(?:`|"|)?(\w+)(?:`|"|)?/i;
66
+ const DROP_INDEX_RE = /DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
67
+ const ADD_COLUMN_RE = /ADD\s+(?:COLUMN\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
68
+ const DROP_COLUMN_RE = /DROP\s+(?:COLUMN\s+)?(?:IF\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
69
+ const ALTER_COLUMN_RE = /ALTER\s+(?:COLUMN\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
70
+ const ADD_CONSTRAINT_RE = /ADD\s+CONSTRAINT\s+(?:`|"|)?(\w+)(?:`|"|)?/i;
71
+ const DROP_CONSTRAINT_RE = /DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?(?:`|"|)?(\w+)(?:`|"|)?/i;
72
+ /**
73
+ * Classify a single SQL statement into a DDLStatement.
74
+ */
75
+ function classifyStatement(raw) {
76
+ const upper = raw.toUpperCase();
77
+ const details = {};
78
+ // CREATE TABLE
79
+ const createTableMatch = raw.match(CREATE_TABLE_RE);
80
+ if (createTableMatch && upper.startsWith("CREATE")) {
81
+ return { type: "CREATE_TABLE", raw, tableName: createTableMatch[1], columnName: null, details };
82
+ }
83
+ // CREATE INDEX
84
+ const createIndexMatch = raw.match(CREATE_INDEX_RE);
85
+ if (createIndexMatch && upper.includes("INDEX")) {
86
+ if (upper.includes("CONCURRENTLY")) {
87
+ details.concurrently = "true";
88
+ }
89
+ if (upper.includes("UNIQUE")) {
90
+ details.unique = "true";
91
+ }
92
+ return { type: "CREATE_INDEX", raw, tableName: createIndexMatch[2], columnName: null, details };
93
+ }
94
+ // DROP TABLE
95
+ const dropTableMatch = raw.match(DROP_TABLE_RE);
96
+ if (dropTableMatch) {
97
+ if (upper.includes("CASCADE"))
98
+ details.cascade = "true";
99
+ return { type: "DROP_TABLE", raw, tableName: dropTableMatch[1], columnName: null, details };
100
+ }
101
+ // DROP INDEX
102
+ const dropIndexMatch = raw.match(DROP_INDEX_RE);
103
+ if (dropIndexMatch && upper.includes("INDEX")) {
104
+ if (upper.includes("CONCURRENTLY"))
105
+ details.concurrently = "true";
106
+ return { type: "DROP_INDEX", raw, tableName: null, columnName: null, details };
107
+ }
108
+ // ALTER TABLE with sub-operations
109
+ const alterTableMatch = raw.match(ALTER_TABLE_RE);
110
+ if (alterTableMatch) {
111
+ const tableName = alterTableMatch[1];
112
+ const afterAlter = raw.substring(alterTableMatch[0].length).trim();
113
+ // ADD CONSTRAINT
114
+ const addConstraintMatch = afterAlter.match(ADD_CONSTRAINT_RE);
115
+ if (addConstraintMatch) {
116
+ details.constraintName = addConstraintMatch[1];
117
+ if (upper.includes("FOREIGN KEY"))
118
+ details.constraintType = "FOREIGN_KEY";
119
+ else if (upper.includes("UNIQUE"))
120
+ details.constraintType = "UNIQUE";
121
+ else if (upper.includes("CHECK"))
122
+ details.constraintType = "CHECK";
123
+ else if (upper.includes("PRIMARY KEY"))
124
+ details.constraintType = "PRIMARY_KEY";
125
+ return { type: "ADD_CONSTRAINT", raw, tableName, columnName: null, details };
126
+ }
127
+ // DROP CONSTRAINT
128
+ const dropConstraintMatch = afterAlter.match(DROP_CONSTRAINT_RE);
129
+ if (dropConstraintMatch) {
130
+ details.constraintName = dropConstraintMatch[1];
131
+ return { type: "DROP_CONSTRAINT", raw, tableName, columnName: null, details };
132
+ }
133
+ // ALTER COLUMN (type change, set not null, drop not null, set default, drop default)
134
+ // Must be checked BEFORE DROP COLUMN — otherwise "ALTER COLUMN x DROP NOT NULL"
135
+ // matches DROP_COLUMN_RE with "NOT" as the column name.
136
+ const alterColMatch = afterAlter.match(ALTER_COLUMN_RE);
137
+ if (alterColMatch && upper.includes("ALTER") && afterAlter.toUpperCase().startsWith("ALTER")) {
138
+ const colName = alterColMatch[1];
139
+ if (upper.includes("SET NOT NULL"))
140
+ details.setNotNull = "true";
141
+ if (upper.includes("DROP NOT NULL"))
142
+ details.dropNotNull = "true";
143
+ if (upper.includes("SET DEFAULT"))
144
+ details.setDefault = "true";
145
+ if (upper.includes("DROP DEFAULT"))
146
+ details.dropDefault = "true";
147
+ if (upper.includes("TYPE") || upper.includes("SET DATA TYPE"))
148
+ details.typeChange = "true";
149
+ return { type: "MODIFY_COLUMN", raw, tableName, columnName: colName, details };
150
+ }
151
+ // DROP COLUMN
152
+ const dropColMatch = afterAlter.match(DROP_COLUMN_RE);
153
+ if (dropColMatch && upper.includes("DROP")) {
154
+ return { type: "DROP_COLUMN", raw, tableName, columnName: dropColMatch[1], details };
155
+ }
156
+ // ADD COLUMN
157
+ const addColMatch = afterAlter.match(ADD_COLUMN_RE);
158
+ if (addColMatch && upper.includes("ADD")) {
159
+ const colName = addColMatch[1];
160
+ if (upper.includes("NOT NULL"))
161
+ details.notNull = "true";
162
+ if (upper.includes("DEFAULT"))
163
+ details.hasDefault = "true";
164
+ return { type: "ADD_COLUMN", raw, tableName, columnName: colName, details };
165
+ }
166
+ // Generic ALTER TABLE (RENAME, etc.)
167
+ if (upper.includes("RENAME")) {
168
+ return { type: "RENAME", raw, tableName, columnName: null, details };
169
+ }
170
+ return { type: "ALTER_TABLE", raw, tableName, columnName: null, details };
171
+ }
172
+ return { type: "OTHER", raw, tableName: null, columnName: null, details };
173
+ }
174
+ /**
175
+ * Parse a complete Flyway migration file.
176
+ */
177
+ export function parseMigration(filename, sql) {
178
+ const { version, description, isRepeatable } = parseFlywayFilename(filename);
179
+ const rawStatements = splitStatements(sql);
180
+ const statements = rawStatements.map(classifyStatement);
181
+ return {
182
+ version,
183
+ description,
184
+ filename,
185
+ isRepeatable,
186
+ statements,
187
+ };
188
+ }
189
+ //# sourceMappingURL=flyway-sql.js.map
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Liquibase XML changelog parser.
3
+ *
4
+ * Parses Liquibase XML changelogs and extracts DDL operations
5
+ * compatible with the same DDLStatement interface used by Flyway.
6
+ */
7
+ /**
8
+ * Parse a Liquibase XML changelog and extract DDL statements.
9
+ *
10
+ * Supports: createTable, dropTable, addColumn, dropColumn, modifyDataType,
11
+ * addNotNullConstraint, createIndex, dropIndex, addForeignKeyConstraint,
12
+ * dropForeignKeyConstraint, renameTable, renameColumn, sql (raw).
13
+ */
14
+ export function parseLiquibaseXml(xml) {
15
+ const changeSets = extractChangeSets(xml);
16
+ const allStatements = [];
17
+ for (const cs of changeSets) {
18
+ allStatements.push(...cs.statements);
19
+ }
20
+ return {
21
+ version: changeSets.length > 0 ? changeSets[0].id : null,
22
+ description: `Liquibase changelog (${changeSets.length} changeSets)`,
23
+ filename: "changelog.xml",
24
+ isRepeatable: false,
25
+ statements: allStatements,
26
+ };
27
+ }
28
+ function extractChangeSets(xml) {
29
+ const results = [];
30
+ const changeSetRe = /<changeSet\s+([^>]*)>([\s\S]*?)<\/changeSet>/gi;
31
+ let match;
32
+ while ((match = changeSetRe.exec(xml)) !== null) {
33
+ const attrs = match[1];
34
+ const body = match[2];
35
+ const id = extractAttr(attrs, "id") || "unknown";
36
+ const author = extractAttr(attrs, "author") || "unknown";
37
+ const statements = parseChangeSetBody(body);
38
+ results.push({ id, author, statements });
39
+ }
40
+ return results;
41
+ }
42
+ function extractAttr(attrs, name) {
43
+ const re = new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i");
44
+ const match = attrs.match(re);
45
+ return match ? match[1] : null;
46
+ }
47
+ function parseChangeSetBody(body) {
48
+ const statements = [];
49
+ // createTable
50
+ const createTableRe = /<createTable\s+([^>]*)(?:\/>|>([\s\S]*?)<\/createTable>)/gi;
51
+ let m;
52
+ while ((m = createTableRe.exec(body)) !== null) {
53
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
54
+ const columnsBody = m[2] || "";
55
+ const columns = extractColumns(columnsBody);
56
+ const details = {};
57
+ if (columns.length > 0)
58
+ details.columns = columns.join(", ");
59
+ statements.push({
60
+ type: "CREATE_TABLE",
61
+ raw: m[0],
62
+ tableName,
63
+ columnName: null,
64
+ details,
65
+ });
66
+ }
67
+ // dropTable
68
+ const dropTableRe = /<dropTable\s+([^/>]*)\/?>(?:<\/dropTable>)?/gi;
69
+ while ((m = dropTableRe.exec(body)) !== null) {
70
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
71
+ const details = {};
72
+ const cascade = extractAttr(m[1], "cascadeConstraints");
73
+ if (cascade === "true")
74
+ details.cascade = "true";
75
+ statements.push({
76
+ type: "DROP_TABLE",
77
+ raw: m[0],
78
+ tableName,
79
+ columnName: null,
80
+ details,
81
+ });
82
+ }
83
+ // addColumn
84
+ const addColumnRe = /<addColumn\s+([^>]*)>([\s\S]*?)<\/addColumn>/gi;
85
+ while ((m = addColumnRe.exec(body)) !== null) {
86
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
87
+ const columnBody = m[2];
88
+ const colRe = /<column\s+([^/>]*)\/?>(?:<\/column>)?/gi;
89
+ let cm;
90
+ while ((cm = colRe.exec(columnBody)) !== null) {
91
+ const colName = extractAttr(cm[1], "name") || "unknown";
92
+ const details = {};
93
+ const colType = extractAttr(cm[1], "type");
94
+ if (colType)
95
+ details.type = colType;
96
+ // Check for constraints
97
+ if (columnBody.includes("nullable=\"false\"") || cm[0].includes("nullable=\"false\"")) {
98
+ details.notNull = "true";
99
+ }
100
+ if (extractAttr(cm[1], "defaultValue") || extractAttr(cm[1], "defaultValueNumeric") || extractAttr(cm[1], "defaultValueBoolean")) {
101
+ details.hasDefault = "true";
102
+ }
103
+ statements.push({
104
+ type: "ADD_COLUMN",
105
+ raw: cm[0],
106
+ tableName,
107
+ columnName: colName,
108
+ details,
109
+ });
110
+ }
111
+ }
112
+ // dropColumn
113
+ const dropColumnRe = /<dropColumn\s+([^/>]*)\/?>(?:<\/dropColumn>)?/gi;
114
+ while ((m = dropColumnRe.exec(body)) !== null) {
115
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
116
+ const colName = extractAttr(m[1], "columnName") || "unknown";
117
+ statements.push({
118
+ type: "DROP_COLUMN",
119
+ raw: m[0],
120
+ tableName,
121
+ columnName: colName,
122
+ details: {},
123
+ });
124
+ }
125
+ // modifyDataType
126
+ const modifyRe = /<modifyDataType\s+([^/>]*)\/?>(?:<\/modifyDataType>)?/gi;
127
+ while ((m = modifyRe.exec(body)) !== null) {
128
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
129
+ const colName = extractAttr(m[1], "columnName") || "unknown";
130
+ const newType = extractAttr(m[1], "newDataType");
131
+ const details = { typeChange: "true" };
132
+ if (newType)
133
+ details.newType = newType;
134
+ statements.push({
135
+ type: "MODIFY_COLUMN",
136
+ raw: m[0],
137
+ tableName,
138
+ columnName: colName,
139
+ details,
140
+ });
141
+ }
142
+ // addNotNullConstraint
143
+ const addNotNullRe = /<addNotNullConstraint\s+([^/>]*)\/?>(?:<\/addNotNullConstraint>)?/gi;
144
+ while ((m = addNotNullRe.exec(body)) !== null) {
145
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
146
+ const colName = extractAttr(m[1], "columnName") || "unknown";
147
+ const details = { setNotNull: "true" };
148
+ const defaultVal = extractAttr(m[1], "defaultNullValue");
149
+ if (defaultVal)
150
+ details.hasDefault = "true";
151
+ statements.push({
152
+ type: "MODIFY_COLUMN",
153
+ raw: m[0],
154
+ tableName,
155
+ columnName: colName,
156
+ details,
157
+ });
158
+ }
159
+ // createIndex
160
+ const createIndexRe = /<createIndex\s+([^>]*)(?:\/>|>([\s\S]*?)<\/createIndex>)/gi;
161
+ while ((m = createIndexRe.exec(body)) !== null) {
162
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
163
+ const details = {};
164
+ if (extractAttr(m[1], "unique") === "true")
165
+ details.unique = "true";
166
+ // Liquibase doesn't have CONCURRENTLY — it's SQL-level
167
+ statements.push({
168
+ type: "CREATE_INDEX",
169
+ raw: m[0],
170
+ tableName,
171
+ columnName: null,
172
+ details,
173
+ });
174
+ }
175
+ // dropIndex
176
+ const dropIndexRe = /<dropIndex\s+([^/>]*)\/?>(?:<\/dropIndex>)?/gi;
177
+ while ((m = dropIndexRe.exec(body)) !== null) {
178
+ statements.push({
179
+ type: "DROP_INDEX",
180
+ raw: m[0],
181
+ tableName: extractAttr(m[1], "tableName") || null,
182
+ columnName: null,
183
+ details: {},
184
+ });
185
+ }
186
+ // addForeignKeyConstraint
187
+ const addFkRe = /<addForeignKeyConstraint\s+([^/>]*)\/?>(?:<\/addForeignKeyConstraint>)?/gi;
188
+ while ((m = addFkRe.exec(body)) !== null) {
189
+ const tableName = extractAttr(m[1], "baseTableName") || "unknown";
190
+ const constraintName = extractAttr(m[1], "constraintName") || "unknown";
191
+ statements.push({
192
+ type: "ADD_CONSTRAINT",
193
+ raw: m[0],
194
+ tableName,
195
+ columnName: null,
196
+ details: { constraintName, constraintType: "FOREIGN_KEY" },
197
+ });
198
+ }
199
+ // dropForeignKeyConstraint
200
+ const dropFkRe = /<dropForeignKeyConstraint\s+([^/>]*)\/?>(?:<\/dropForeignKeyConstraint>)?/gi;
201
+ while ((m = dropFkRe.exec(body)) !== null) {
202
+ const tableName = extractAttr(m[1], "baseTableName") || "unknown";
203
+ const constraintName = extractAttr(m[1], "constraintName") || "unknown";
204
+ statements.push({
205
+ type: "DROP_CONSTRAINT",
206
+ raw: m[0],
207
+ tableName,
208
+ columnName: null,
209
+ details: { constraintName },
210
+ });
211
+ }
212
+ // renameTable
213
+ const renameTableRe = /<renameTable\s+([^/>]*)\/?>(?:<\/renameTable>)?/gi;
214
+ while ((m = renameTableRe.exec(body)) !== null) {
215
+ const oldName = extractAttr(m[1], "oldTableName");
216
+ const newName = extractAttr(m[1], "newTableName");
217
+ statements.push({
218
+ type: "RENAME",
219
+ raw: m[0],
220
+ tableName: oldName || "unknown",
221
+ columnName: null,
222
+ details: { newName: newName || "unknown" },
223
+ });
224
+ }
225
+ // renameColumn
226
+ const renameColRe = /<renameColumn\s+([^/>]*)\/?>(?:<\/renameColumn>)?/gi;
227
+ while ((m = renameColRe.exec(body)) !== null) {
228
+ const tableName = extractAttr(m[1], "tableName") || "unknown";
229
+ const oldName = extractAttr(m[1], "oldColumnName");
230
+ const newName = extractAttr(m[1], "newColumnName");
231
+ statements.push({
232
+ type: "RENAME",
233
+ raw: m[0],
234
+ tableName,
235
+ columnName: oldName || null,
236
+ details: { newName: newName || "unknown" },
237
+ });
238
+ }
239
+ // Raw SQL
240
+ const sqlRe = /<sql>([\s\S]*?)<\/sql>/gi;
241
+ while ((m = sqlRe.exec(body)) !== null) {
242
+ statements.push({
243
+ type: "OTHER",
244
+ raw: m[1].trim(),
245
+ tableName: null,
246
+ columnName: null,
247
+ details: { source: "inline-sql" },
248
+ });
249
+ }
250
+ return statements;
251
+ }
252
+ function extractColumns(body) {
253
+ const cols = [];
254
+ const colRe = /<column\s+([^/>]*)\/?>(?:<\/column>)?/gi;
255
+ let m;
256
+ while ((m = colRe.exec(body)) !== null) {
257
+ const name = extractAttr(m[1], "name");
258
+ if (name)
259
+ cols.push(name);
260
+ }
261
+ return cols;
262
+ }
263
+ //# sourceMappingURL=liquibase-xml.js.map