UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

313 lines 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DDLDiffGenerator = void 0; const SqlParser_1 = require("../parsers/SqlParser"); const SqlFormatter_1 = require("./SqlFormatter"); const DDLGeneralizer_1 = require("./DDLGeneralizer"); const MultiQuerySplitter_1 = require("../utils/MultiQuerySplitter"); const CreateTableQuery_1 = require("../models/CreateTableQuery"); const DDLStatements_1 = require("../models/DDLStatements"); const ValueComponent_1 = require("../models/ValueComponent"); class DDLDiffGenerator { static generateDiff(currentSql, expectedSql, options = {}) { const currentAst = this.parseAndGeneralize(currentSql); const expectedAst = this.parseAndGeneralize(expectedSql); const currentSchema = this.buildSchema(currentAst); const expectedSchema = this.buildSchema(expectedAst); const diffAsts = []; // Compare Tables for (const [tableName, expectedTable] of expectedSchema.tables) { const currentTable = currentSchema.tables.get(tableName); if (!currentTable) { // Table missing in current -> Create it // We reconstruct the CreateTableQuery from columns const columns = Array.from(expectedTable.columns.values()).map(c => c.definition); const tableNameStr = expectedTable.qualifiedName.name instanceof ValueComponent_1.RawString ? expectedTable.qualifiedName.name.value : expectedTable.qualifiedName.name.name; const namespaces = expectedTable.qualifiedName.namespaces ? expectedTable.qualifiedName.namespaces.map(ns => ns.name) : null; const createTable = new CreateTableQuery_1.CreateTableQuery({ tableName: tableNameStr, namespaces: namespaces, columns: columns }); diffAsts.push(createTable); // And add constraints for (const constraint of expectedTable.constraints) { diffAsts.push(new DDLStatements_1.AlterTableStatement({ table: expectedTable.qualifiedName, actions: [new DDLStatements_1.AlterTableAddConstraint({ constraint: constraint.definition })] })); } // And add indexes for (const index of expectedTable.indexes) { diffAsts.push(index.definition); } } else { // Table exists -> Compare columns and constraints this.compareColumns(currentTable, expectedTable, diffAsts, options); this.compareConstraints(currentTable, expectedTable, diffAsts, options); this.compareIndexes(currentTable, expectedTable, diffAsts, options); } } // Drop Tables (if enabled) if (options.dropTables) { for (const [tableName, currentTable] of currentSchema.tables) { if (!expectedSchema.tables.has(tableName)) { // Table exists in current but not in expected -> Drop it // We need a DropTableStatement. For now, we can manually construct the SQL or add DropTableStatement model. // Since we return string[], we can just push a raw SQL string if we don't have the AST model yet, // OR better, let's use a simple object that formats to DROP TABLE. // But wait, the return type is string[] derived from ASTs. // Let's assume we can use a raw SQL component or similar. // Actually, let's just use a simple custom AST node or formatted string injection if possible. // Looking at imports, we don't have DropTableStatement. // Let's add a temporary workaround or just return the string directly? // The method returns string[] by mapping diffAsts. // We should add a DropTableStatement class or similar. // For now, let's just push a dummy component that formats to DROP TABLE. // Actually, let's check if we can import DropTableStatement. // It seems it's not imported. Let's check DDLStatements.ts. // If we can't easily add the AST, we might need to hack it or add the class. // Let's try to add a simple DropTableStatement to DDLStatements.ts first if needed. // But for now, let's assume we can just append the string at the end? // No, the return is `diffAsts.map(...)`. // Let's create a simple ad-hoc object that satisfies SqlComponent and formats correctly. diffAsts.push(new DDLStatements_1.DropTableStatement({ tables: [currentTable.qualifiedName], ifExists: false })); } } } // Format output const formatter = new SqlFormatter_1.SqlFormatter(options.formatOptions || { keywordCase: 'upper' }); return diffAsts.map(ast => formatter.format(ast).formattedSql + ';'); } static parseAndGeneralize(sql) { const split = MultiQuerySplitter_1.MultiQuerySplitter.split(sql); const asts = []; for (const q of split.queries) { if (q.isEmpty) continue; try { const ast = SqlParser_1.SqlParser.parse(q.sql); asts.push(ast); } catch (e) { // Ignore parse errors? Or throw? // For diffing, we probably want to know if input is invalid. console.warn("Failed to parse SQL for diff:", q.sql, e); } } return DDLGeneralizer_1.DDLGeneralizer.generalize(asts); } static buildSchema(asts) { var _a; const tables = new Map(); const formatter = new SqlFormatter_1.SqlFormatter({ keywordCase: 'none' }); for (const ast of asts) { if (ast instanceof CreateTableQuery_1.CreateTableQuery) { const qName = new ValueComponent_1.QualifiedName(ast.namespaces || [], ast.tableName); const key = this.getQualifiedNameKey(qName); const tableModel = { name: key, qualifiedName: qName, columns: new Map(), constraints: [], indexes: [] }; for (const col of ast.columns) { tableModel.columns.set(col.name.name, { name: col.name.name, definition: col }); } // Generalized CreateTable shouldn't have tableConstraints, but if it did, we'd handle them. tables.set(key, tableModel); } else if (ast instanceof DDLStatements_1.AlterTableStatement) { const key = this.getQualifiedNameKey(ast.table); const tableModel = tables.get(key); if (tableModel) { for (const action of ast.actions) { if (action instanceof DDLStatements_1.AlterTableAddConstraint) { const formatted = formatter.format(action.constraint).formattedSql; tableModel.constraints.push({ name: (_a = action.constraint.constraintName) === null || _a === void 0 ? void 0 : _a.name, kind: action.constraint.kind, definition: action.constraint, formatted: formatted }); } else if (action instanceof DDLStatements_1.AlterTableAddColumn) { tableModel.columns.set(action.column.name.name, { name: action.column.name.name, definition: action.column }); } } } } else if (ast instanceof DDLStatements_1.CreateIndexStatement) { const key = this.getQualifiedNameKey(ast.tableName); const tableModel = tables.get(key); if (tableModel) { const formatted = formatter.format(ast).formattedSql; tableModel.indexes.push({ name: ast.indexName.toString(), definition: ast, formatted: formatted }); } } } return { tables }; } static compareColumns(current, expected, diffs, options) { // Add missing columns for (const [name, col] of expected.columns) { if (!current.columns.has(name)) { diffs.push(new DDLStatements_1.AlterTableStatement({ table: expected.qualifiedName, actions: [new DDLStatements_1.AlterTableAddColumn({ column: col.definition })] })); } } // Drop extra columns if (options.dropColumns) { for (const [name, col] of current.columns) { if (!expected.columns.has(name)) { diffs.push(new DDLStatements_1.AlterTableStatement({ table: expected.qualifiedName, actions: [new DDLStatements_1.AlterTableDropColumn({ columnName: col.definition.name })] })); } } } } static compareConstraints(current, expected, diffs, options) { // We need to match constraints. // If checkConstraintNames is true, match by name. // Else, match by formatted definition (ignoring name in formatting if possible, but SqlFormatter prints name). // To compare by definition ignoring name, we might need to strip name from definition before formatting. const formatter = new SqlFormatter_1.SqlFormatter({ keywordCase: 'none' }); const getConstraintSignature = (c) => { if (options.checkConstraintNames) { // Special handling for PRIMARY KEY: ignore name difference if (c.kind === 'primary-key') { // Strip "CONSTRAINT name" prefix, handling both quoted and unquoted names // Match: CONSTRAINT "name" or CONSTRAINT name (case-insensitive) const sig = c.formatted.replace(/^constraint\s+("[^"]+"|[^\s]+)\s+/i, '').trim(); return sig; } return c.name || c.formatted; // Fallback if no name? } // Remove name from definition for comparison // "CONSTRAINT name PRIMARY KEY ..." vs "PRIMARY KEY ..." // If we format it, it might include CONSTRAINT name. // We can regex remove it? return c.formatted.replace(/^constraint\s+("[^"]+"|[^\s]+)\s+/i, '').trim(); }; const currentSignatures = new Set(current.constraints.map(getConstraintSignature)); // Add missing constraints for (const expectedC of expected.constraints) { const sig = getConstraintSignature(expectedC); if (!currentSignatures.has(sig)) { diffs.push(new DDLStatements_1.AlterTableStatement({ table: expected.qualifiedName, actions: [new DDLStatements_1.AlterTableAddConstraint({ constraint: expectedC.definition })] })); } } // Drop extra constraints if (options.dropConstraints) { const expectedSignatures = new Set(expected.constraints.map(getConstraintSignature)); for (const currentC of current.constraints) { const sig = getConstraintSignature(currentC); if (!expectedSignatures.has(sig)) { // To drop, we need a name. If no name, we can't drop easily (DBs usually auto-name). if (currentC.name) { diffs.push(new DDLStatements_1.AlterTableStatement({ table: expected.qualifiedName, actions: [new DDLStatements_1.AlterTableDropConstraint({ constraintName: new ValueComponent_1.IdentifierString(currentC.name) })] })); } else { console.warn("Cannot drop unnamed constraint:", currentC.formatted); } } } } } static compareIndexes(current, expected, diffs, options) { const getIndexSignature = (idx) => { if (options.checkConstraintNames) { // When Check Names is enabled, index name matters return idx.name; } // When Check Names is disabled, compare by structural properties from AST // Compare: table name, columns (expressions), unique flag, using method, where clause const def = idx.definition; const parts = []; // Table name parts.push(def.tableName.toString()); // Unique flag if (def.unique) { parts.push('UNIQUE'); } // Using method (e.g., BTREE, HASH) if (def.usingMethod) { parts.push(`USING:${def.usingMethod.toString()}`); } // Columns (expressions and sort orders) const columnSigs = def.columns.map(col => { const expr = col.expression.toString(); const sort = col.sortOrder || ''; const nulls = col.nullsOrder || ''; return `${expr}${sort}${nulls}`; }); parts.push(`COLS:${columnSigs.join(',')}`); // Include columns if (def.include && def.include.length > 0) { parts.push(`INCLUDE:${def.include.map(i => i.toString()).join(',')}`); } // Where clause if (def.where) { parts.push(`WHERE:${def.where.toString()}`); } return parts.join('|'); }; const currentSignatures = new Set(current.indexes.map(getIndexSignature)); // Add missing indexes for (const expectedIdx of expected.indexes) { const sig = getIndexSignature(expectedIdx); if (!currentSignatures.has(sig)) { diffs.push(expectedIdx.definition); } } // Drop extra indexes // When checkConstraintNames is enabled, we should drop indexes with different names // When dropIndexes is enabled, we should drop all extra indexes if (options.checkConstraintNames || options.dropIndexes) { const expectedSignatures = new Set(expected.indexes.map(getIndexSignature)); for (const currentIdx of current.indexes) { const sig = getIndexSignature(currentIdx); if (!expectedSignatures.has(sig)) { diffs.push(new DDLStatements_1.DropIndexStatement({ indexNames: [currentIdx.definition.indexName], ifExists: false })); } } } } static getQualifiedNameKey(qName) { return qName.toString(); } } exports.DDLDiffGenerator = DDLDiffGenerator; //# sourceMappingURL=DDLDiffGenerator.js.map