UNPKG

query-analyzer

Version:

A utility for enhancing Sequelize queries with EXPLAIN options and logging detailed query plans.

166 lines (165 loc) 8.57 kB
"use strict"; 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; }