UNPKG

ts-mysql-analyzer

Version:

A MySQL query analyzer.

213 lines 10.5 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const ts_mysql_parser_1 = __importStar(require("ts-mysql-parser")); const invalid_assignment_1 = require("./lib/invalid-assignment"); const get_schema_column_1 = require("./lib/get-schema-column"); const get_schema_table_1 = require("./lib/get-schema-table"); const missing_index_1 = require("./lib/missing-index"); const autocorrect_1 = require("./lib/autocorrect"); /** Represents the severity of the diagnostic */ var DiagnosticSeverity; (function (DiagnosticSeverity) { /** Something suspicious but allowed */ DiagnosticSeverity[DiagnosticSeverity["Warning"] = 0] = "Warning"; /** Something not allowed by any means */ DiagnosticSeverity[DiagnosticSeverity["Error"] = 1] = "Error"; /** Something to suggest a better way of doing things */ DiagnosticSeverity[DiagnosticSeverity["Suggestion"] = 2] = "Suggestion"; })(DiagnosticSeverity = exports.DiagnosticSeverity || (exports.DiagnosticSeverity = {})); /** Represents the code of the diagnostic */ var DiagnosticCode; (function (DiagnosticCode) { /** An empty MySQL query */ DiagnosticCode[DiagnosticCode["EmptyQuery"] = 1000] = "EmptyQuery"; /** A query that contains a lexer error */ DiagnosticCode[DiagnosticCode["LexerError"] = 1001] = "LexerError"; /** A query that contains a parser error */ DiagnosticCode[DiagnosticCode["ParserError"] = 1002] = "ParserError"; /** A mismatch in the number of rows and columns in an INSERT statement */ DiagnosticCode[DiagnosticCode["ColumnRowMismatch"] = 1003] = "ColumnRowMismatch"; /** A table reference that does not exist in the schema */ DiagnosticCode[DiagnosticCode["MissingTable"] = 1004] = "MissingTable"; /** A column reference that does not exist in the referenced table in the schema */ DiagnosticCode[DiagnosticCode["MissingColumn"] = 1005] = "MissingColumn"; /** An invalid type assignment */ DiagnosticCode[DiagnosticCode["TypeMismatch"] = 1006] = "TypeMismatch"; /** A missing database index for a referenced column */ DiagnosticCode[DiagnosticCode["MissingIndex"] = 1007] = "MissingIndex"; })(DiagnosticCode = exports.DiagnosticCode || (exports.DiagnosticCode = {})); class MySQLAnalyzer { constructor(options = {}) { this.parserOptions = options.parserOptions; this.schema = options.schema; } analyze(text) { if (text === '') { return [ { severity: DiagnosticSeverity.Error, message: 'MySQL query is empty.', start: 0, stop: text.length, code: DiagnosticCode.EmptyQuery } ]; } let diagnostics = []; const parser = new ts_mysql_parser_1.default(this.parserOptions); const statements = parser.splitStatements(text); for (const statement of statements) { const result = parser.parse(statement.text); diagnostics = diagnostics.concat(this.analyzeSyntax(statement, result)); diagnostics = diagnostics.concat(this.analyzeSemantics(statement, result, parser)); } return diagnostics; } analyzeSyntax(statement, result) { const diagnostics = []; if (result.lexerError) { diagnostics.push({ severity: DiagnosticSeverity.Error, message: result.lexerError.message, start: statement.start, stop: statement.stop, code: DiagnosticCode.LexerError }); } if (result.parserError) { const { offendingToken } = result.parserError.data; diagnostics.push({ severity: DiagnosticSeverity.Error, message: result.parserError.message, start: statement.start + ((offendingToken === null || offendingToken === void 0 ? void 0 : offendingToken.startIndex) || 0), stop: statement.start + ((offendingToken === null || offendingToken === void 0 ? void 0 : offendingToken.stopIndex) || 0), code: DiagnosticCode.ParserError }); } return diagnostics; } analyzeSemantics(statement, result, parser) { let diagnostics = []; if (parser.isDDL(result)) { return diagnostics; } if (parser.getQueryType(result) === ts_mysql_parser_1.MySQLQueryType.QtInsert) { const { columnReferences, valueReferences } = result.references; const fieldsClauseRefs = columnReferences.filter(r => r.context === 'fieldsClause'); const valuesClauseValues = valueReferences.filter(r => r.context === 'valuesClause'); if (fieldsClauseRefs.length !== valuesClauseValues.length) { diagnostics.push({ severity: DiagnosticSeverity.Warning, message: 'Column count does not match row count.', start: statement.start, stop: statement.stop, code: DiagnosticCode.ColumnRowMismatch }); } } if (!this.schema) { return diagnostics; } const tableDiagnostics = this.analyzeTables(statement, result.references); diagnostics = diagnostics.concat(tableDiagnostics); return diagnostics; } analyzeTables(statement, references) { var _a, _b; let diagnostics = []; if (!this.schema) { return diagnostics; } const databaseName = (_a = this.schema) === null || _a === void 0 ? void 0 : _a.config.schema; const { tableReferences, columnReferences, valueReferences, aliasReferences } = references; for (const tableRef of tableReferences) { const { table, start, stop } = tableRef; const schemaTable = get_schema_table_1.getSchemaTable(table, this.schema.tables, aliasReferences); if (schemaTable) { const columnRefs = columnReferences.filter(r => { var _a; return ((_a = r.tableReference) === null || _a === void 0 ? void 0 : _a.table) === table; }); const valueRefs = valueReferences.filter(r => { var _a, _b; return ((_b = (_a = r.columnReference) === null || _a === void 0 ? void 0 : _a.tableReference) === null || _b === void 0 ? void 0 : _b.table) === table; }); const columnDiagnostics = this.analyzeColumns(statement, schemaTable, columnRefs, valueRefs, aliasReferences); diagnostics = diagnostics.concat(columnDiagnostics); } else { const messageParts = [`Table '${table}' does not exist in database '${databaseName}'.`]; const tableNames = ((_b = this.schema) === null || _b === void 0 ? void 0 : _b.tables.map(t => t.name)) || []; const correction = autocorrect_1.getCorrection(table.toLowerCase(), tableNames); if (correction) { messageParts.push(` Did you mean '${correction}'?`); } diagnostics.push({ severity: DiagnosticSeverity.Warning, message: messageParts.join(''), start: statement.start + start, stop: statement.start + stop, code: DiagnosticCode.MissingTable }); } } return diagnostics; } analyzeColumns(statement, schemaTable, columnRefs, valueRefs, aliasRefs) { let diagnostics = []; const { name: tableName } = schemaTable; for (const columnRef of columnRefs) { const { column, start, stop } = columnRef; const schemaColumn = get_schema_column_1.getSchemaColumn(column, schemaTable.columns, aliasRefs); if (schemaColumn) { const valueRef = valueRefs.find(r => { var _a; return ((_a = r.columnReference) === null || _a === void 0 ? void 0 : _a.column) === column; }); const columnDiagnostics = this.analyzeColumn(statement, schemaColumn, columnRef, valueRef); diagnostics = diagnostics.concat(columnDiagnostics); } else { const messageParts = [`Column '${column}' does not exist in table '${tableName}'.`]; const columnNames = schemaTable.columns.map(c => c.name); const correction = autocorrect_1.getCorrection(column.toLowerCase(), columnNames); if (correction) { messageParts.push(` Did you mean '${correction}'?`); } diagnostics.push({ severity: DiagnosticSeverity.Warning, message: messageParts.join(''), start: statement.start + start, stop: statement.start + stop, code: DiagnosticCode.MissingColumn }); } } return diagnostics; } analyzeColumn(statement, schemaColumn, columnRef, valueRef) { const diagnostics = []; if (!valueRef) { return diagnostics; } if (invalid_assignment_1.invalidAssignment(schemaColumn, valueRef)) { diagnostics.push({ severity: DiagnosticSeverity.Warning, message: `Type ${valueRef.dataType} is not assignable to type ${schemaColumn.tsType}.`, start: statement.start + valueRef.start, stop: statement.start + valueRef.stop, code: DiagnosticCode.TypeMismatch }); } if (missing_index_1.missingIndex(schemaColumn, valueRef)) { diagnostics.push({ severity: DiagnosticSeverity.Suggestion, message: `You can optimize this query by adding a MySQL index for column '${schemaColumn.name}'.`, start: statement.start + columnRef.start, stop: statement.start + columnRef.stop, code: DiagnosticCode.MissingIndex }); } return diagnostics; } } exports.MySQLAnalyzer = MySQLAnalyzer; //# sourceMappingURL=index.js.map