ts-mysql-analyzer
Version:
A MySQL query analyzer.
213 lines • 10.5 kB
JavaScript
"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