query-analyzer
Version:
A utility for enhancing Sequelize queries with EXPLAIN options and logging detailed query plans.
166 lines (165 loc) • 8.57 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.enableAnalyzer = enableAnalyzer;
const date_fns_1 = require("date-fns");
const csvUtil_1 = require("./csvUtil");
const errors_1 = require("./errors");
/**
* Enable query analyzer for a Sequelize instance
* @param sequelize Sequelize instance to analyze
* @param options Configuration options
*/
function enableAnalyzer(sequelize_1) {
return __awaiter(this, arguments, void 0, function* (sequelize, options = {}) {
// Check if analyzer should be enabled
const isEnabled = shouldEnableAnalyzer(options);
const originalQuery = sequelize.query.bind(sequelize);
sequelize.query = function (...args) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
// Skip analysis if disabled
if (!isEnabled) {
return originalQuery.apply(null, args);
}
let query = '';
if (typeof args[0] === 'object' && args[0].query) {
query = args[0].query;
}
else if (typeof args[0] === 'string') {
query = args[0];
}
try {
if (query.startsWith('EXPLAIN') || query.startsWith('START') || query.startsWith('ROLLBACK') || query.startsWith('COMMIT')) {
return originalQuery.apply(null, args);
}
const queryStartTime = Date.now();
const results = yield originalQuery.apply(null, args);
const actualExecutionTime = Date.now() - queryStartTime;
try {
const payload = {
query,
actualExecutionTime,
queryPlan: '',
planningTime: '',
executionTime: '',
startCost: '',
endCost: '',
params: undefined,
};
let reportType = 'EXPLAIN (ANALYZE)';
if (query.match(/\b(CALL)\b/)) {
reportType = 'NONE';
}
else if (query.match(/\b(UPDATE|DELETE|INSERT)\b/i)) {
reportType = 'EXPLAIN';
if (args[0]) {
payload.params = JSON.stringify(args[0].bind || {}, null, 2);
}
}
else {
if (args[1]) {
args[1].type = 'RAW';
payload.params = JSON.stringify(args[1].replacements || {}, null, 2);
}
const explainOptions = ['ANALYZE'];
if (options.verbose)
explainOptions.push('VERBOSE');
if (options.costs)
explainOptions.push('COSTS');
if (options.settings)
explainOptions.push('SETTINGS');
if (options.buffers)
explainOptions.push('BUFFERS');
if (options.serialize)
explainOptions.push(`SERIALIZE ${options.serialize}`);
if (options.wal)
explainOptions.push('WAL');
if (options.timing)
explainOptions.push('TIMING');
if (options.summary)
explainOptions.push('SUMMARY');
reportType = `EXPLAIN (${explainOptions.join(', ')})`;
}
if (reportType.startsWith('EXPLAIN')) {
if (typeof args[0] === 'object') {
args[0].query = reportType + ' ' + query;
}
else if (typeof args[0] === 'string') {
args[0] = reportType + ' ' + query;
}
const queryResult = (yield originalQuery.apply(null, args));
payload.queryPlan = queryResult.flat().map(item => item['QUERY PLAN']).join('\n');
const costMatch = payload.queryPlan.match(/cost=([\d.]+)\.\.([\d.]+)/);
payload.startCost = costMatch ? costMatch[1] : 'N/A';
payload.endCost = costMatch ? costMatch[2] : 'N/A';
const execTimeMatch = payload.queryPlan.match(/Execution Time: (\d+\.\d+) /);
payload.executionTime = execTimeMatch ? parseFloat(execTimeMatch[1]).toFixed(2) : 'N/A';
const planningTimeMatch = payload.queryPlan.match(/Planning Time: (\d+\.\d+) /);
payload.planningTime = planningTimeMatch ? parseFloat(planningTimeMatch[1]).toFixed(2) : 'N/A';
}
try {
yield (0, csvUtil_1.appendCsv)(`analyzer/report-${(0, date_fns_1.format)(new Date(), 'yyyy-MM-dd')}.csv`, payload);
}
catch (csvError) {
const error = new errors_1.CsvError(query, csvError instanceof Error ? csvError : undefined);
console.error(error.getFormattedMessage());
if (options.onError) {
yield Promise.resolve(options.onError(error));
}
}
if (options.onSlowQuery) {
const threshold = (_a = options.slowQueryThreshold) !== null && _a !== void 0 ? _a : 1000; // Default: 1000ms
if (actualExecutionTime >= threshold) {
yield Promise.resolve(options.onSlowQuery(payload));
}
}
}
catch (e) {
const error = new errors_1.ExplainError(query, e instanceof Error ? e : undefined);
console.error(error.getFormattedMessage());
if (options.onError) {
yield Promise.resolve(options.onError(error));
}
}
return results;
}
catch (error) {
const queryError = new errors_1.QueryExecutionError(query, error instanceof Error ? error : undefined);
console.error(queryError.getFormattedMessage());
throw error; // Re-throw original error to maintain backward compatibility
}
});
};
});
}
/**
* Determines if the analyzer should be enabled based on configuration options
* @param options AnalyzerOptions configuration
* @returns true if analyzer should run, false otherwise
*/
function shouldEnableAnalyzer(options) {
// Check explicit enabled flag first
if (options.enabled === false) {
return false;
}
// If enabled is explicitly true, enable regardless of environment
if (options.enabled === true) {
return true;
}
if (options.environment) {
const currentEnv = process.env.NODE_ENV || 'development';
if (currentEnv !== options.environment) {
return false;
}
}
return true;
}