import _ from 'lodash'; import sqlPart from './sql_part'; export default class PostgresQuery { target: any; selectModels: any[]; queryBuilder: any; groupByParts: any[]; whereParts: any[]; templateSrv: any; scopedVars: any; /** @ngInject */ constructor(target, templateSrv?, scopedVars?) { this.target = target; this.templateSrv = templateSrv; this.scopedVars = scopedVars; target.schema = target.schema || 'public'; target.format = target.format || 'time_series'; target.timeColumn = target.timeColumn || 'time'; target.metricColumn = target.metricColumn || 'None'; target.groupBy = target.groupBy || []; target.where = target.where || []; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; // handle pre query gui panels gracefully if (!('rawQuery' in this.target)) { if ('rawSql' in target) { // pre query gui panel target.rawQuery = true; } else { // new panel target.rawQuery = false; } } // give interpolateQueryStr access to this this.interpolateQueryStr = this.interpolateQueryStr.bind(this); this.updateProjection(); } // remove identifier quoting from identifier to use in metadata queries unquoteIdentifier(value) { if (value[0] === '"' && value[value.length - 1] === '"') { return value.substring(1, value.length - 1).replace('""', '"'); } else { return value; } } quoteIdentifier(value) { return '"' + value.replace('"', '""') + '"'; } quoteLiteral(value) { return "'" + value.replace("'", "''") + "'"; } updateProjection() { this.selectModels = _.map(this.target.select, function(parts: any) { return _.map(parts, sqlPart.create).filter(n => n); }); this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n); this.groupByParts = _.map(this.target.groupBy, sqlPart.create).filter(n => n); } updatePersistedParts() { this.target.select = _.map(this.selectModels, function(selectParts) { return _.map(selectParts, function(part: any) { return { type: part.def.type, params: part.params }; }); }); this.target.where = _.map(this.whereParts, function(part: any) { return { type: part.def.type, name: part.name, params: part.params }; }); this.target.groupBy = _.map(this.groupByParts, function(part: any) { return { type: part.def.type, params: part.params }; }); } hasGroupByTime() { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } addGroupBy(partType, value) { var partModel = sqlPart.create({ type: partType, params: [value] }); var partCount = this.target.groupBy.length; if (partCount === 0) { this.target.groupBy.push(partModel.part); } else if (partType === 'time') { // put timeGroup at start this.target.groupBy.splice(0, 0, partModel.part); } else { this.target.groupBy.push(partModel.part); } if (partType === 'time') { partModel.part.params = ['1m', 'none']; } // add aggregates when adding group by for (let i = 0; i < this.target.select.length; i++) { var selectParts = this.target.select[i]; if (!selectParts.some(part => part.type === 'aggregate')) { selectParts.splice(1, 0, { type: 'aggregate', params: ['avg'] }); if (!selectParts.some(part => part.type === 'alias')) { selectParts.push({ type: 'alias', params: [selectParts[0].params[0]] }); } } } this.updateProjection(); } removeGroupByPart(part, index) { if (part.def.type === 'time') { // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { if (part.type === 'aggregate') { return false; } return true; }); }); } this.target.groupBy.splice(index, 1); this.updateProjection(); } removeSelectPart(selectParts, part) { // if we remove the field remove the whole statement if (part.def.type === 'column') { if (this.selectModels.length > 1) { var modelsIndex = _.indexOf(this.selectModels, selectParts); this.selectModels.splice(modelsIndex, 1); } } else { var partIndex = _.indexOf(selectParts, part); selectParts.splice(partIndex, 1); } this.updatePersistedParts(); } addSelectPart(selectParts, type) { var partModel = sqlPart.create({ type: type }); partModel.def.addStrategy(selectParts, partModel, this); this.updatePersistedParts(); } interpolateQueryStr(value, variable, defaultFormatFn) { // if no multi or include all do not regexEscape if (!variable.multi && !variable.includeAll) { return value; } if (typeof value === 'string') { return this.quoteLiteral(value); } var escapedValues = _.map(value, this.quoteLiteral); return '(' + escapedValues.join(',') + ')'; } render(interpolate?) { let target = this.target; let query; if (target.rawQuery) { if (interpolate) { return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr); } else { return target.rawSql; } } query = this.buildQuery(target); if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); } return query; } buildTimeColumn(target) { let timeGroup = this.hasGroupByTime(); let query; if (timeGroup) { var args; if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') { args = timeGroup.params.join(','); } else { args = timeGroup.params[0]; } query = '$__timeGroup(' + target.timeColumn + ',' + args + ')'; } else { query = target.timeColumn + ' AS "time"'; } return query; } buildValueColumns(target) { let query = ''; for (let i = 0; i < this.selectModels.length; i++) { let parts = this.selectModels[i]; var selectText = ''; for (let y = 0; y < parts.length; y++) { let part = parts[y]; selectText = part.render(selectText); } query += ', ' + selectText; } return query; } buildWhereClause(target) { let query = ''; var conditions = _.map(target.where, (tag, index) => { switch (tag.type) { case 'macro': return tag.name + '(' + target.timeColumn + ')'; break; case 'expression': return tag.params.join(' '); break; } }); if (conditions.length > 0) { query = ' WHERE ' + conditions.join(' AND '); } return query; } buildQuery(target) { var query = 'SELECT '; query += this.buildTimeColumn(target); if (this.target.metricColumn !== 'None') { query += ',' + this.target.metricColumn + ' AS metric'; } query += this.buildValueColumns(target); query += ' FROM ' + target.schema + '.' + target.table; query += this.buildWhereClause(target); var groupBySection = ''; for (let i = 0; i < this.groupByParts.length; i++) { var part = this.groupByParts[i]; if (i > 0) { groupBySection += ', '; } if (part.def.type === 'time') { groupBySection += '1'; } else { groupBySection += part.render(''); } } if (groupBySection.length) { query += ' GROUP BY ' + groupBySection; if (this.target.metricColumn !== 'None') { query += ',2'; } } query += ' ORDER BY 1'; this.target.rawSql = query; return query; } }