123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- import { auto } from 'angular';
- import { clone, filter, find, findIndex, indexOf, map } from 'lodash';
- import { PanelEvents, QueryResultMeta } from '@grafana/data';
- import { TemplateSrv } from '@grafana/runtime';
- import { SqlPart } from 'app/angular/components/sql_part/sql_part';
- import appEvents from 'app/core/app_events';
- import { VariableWithMultiSupport } from 'app/features/variables/types';
- import { QueryCtrl } from 'app/plugins/sdk';
- import { ShowConfirmModalEvent } from 'app/types/events';
- import { PostgresMetaQuery } from './meta_query';
- import PostgresQueryModel from './postgres_query_model';
- import sqlPart from './sql_part';
- const defaultQuery = `SELECT
- $__time(time_column),
- value1
- FROM
- metric_table
- WHERE
- $__timeFilter(time_column)
- `;
- export class PostgresQueryCtrl extends QueryCtrl {
- static templateUrl = 'partials/query.editor.html';
- formats: any[];
- queryModel: PostgresQueryModel;
- metaBuilder: PostgresMetaQuery;
- lastQueryMeta?: QueryResultMeta;
- lastQueryError?: string;
- showHelp = false;
- tableSegment: any;
- whereAdd: any;
- timeColumnSegment: any;
- metricColumnSegment: any;
- selectMenu: any[] = [];
- selectParts: SqlPart[][] = [[]];
- groupParts: SqlPart[] = [];
- whereParts: SqlPart[] = [];
- groupAdd: any;
- /** @ngInject */
- constructor(
- $scope: any,
- $injector: auto.IInjectorService,
- private templateSrv: TemplateSrv,
- private uiSegmentSrv: any
- ) {
- super($scope, $injector);
- this.target = this.target;
- this.queryModel = new PostgresQueryModel(this.target, templateSrv, this.panel.scopedVars);
- this.metaBuilder = new PostgresMetaQuery(this.target, this.queryModel);
- this.updateProjection();
- this.formats = [
- { text: 'Time series', value: 'time_series' },
- { text: 'Table', value: 'table' },
- ];
- if (!this.target.rawSql) {
- // special handling when in table panel
- if (this.panelCtrl.panel.type === 'table') {
- this.target.format = 'table';
- this.target.rawSql = 'SELECT 1';
- this.target.rawQuery = true;
- } else {
- this.target.rawSql = defaultQuery;
- this.datasource.metricFindQuery(this.metaBuilder.findMetricTable()).then((result: any) => {
- if (result.length > 0) {
- this.target.table = result[0].text;
- let segment = this.uiSegmentSrv.newSegment(this.target.table);
- this.tableSegment.html = segment.html;
- this.tableSegment.value = segment.value;
- this.target.timeColumn = result[1].text;
- segment = this.uiSegmentSrv.newSegment(this.target.timeColumn);
- this.timeColumnSegment.html = segment.html;
- this.timeColumnSegment.value = segment.value;
- this.target.timeColumnType = 'timestamp';
- this.target.select = [[{ type: 'column', params: [result[2].text] }]];
- this.updateProjection();
- this.updateRawSqlAndRefresh();
- }
- });
- }
- }
- if (!this.target.table) {
- this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true });
- } else {
- this.tableSegment = uiSegmentSrv.newSegment(this.target.table);
- }
- this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn);
- this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn);
- this.buildSelectMenu();
- this.whereAdd = this.uiSegmentSrv.newPlusButton();
- this.groupAdd = this.uiSegmentSrv.newPlusButton();
- this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope);
- this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
- }
- updateRawSqlAndRefresh() {
- if (!this.target.rawQuery) {
- this.target.rawSql = this.queryModel.buildQuery();
- }
- this.panelCtrl.refresh();
- }
- timescaleAggCheck() {
- const aggIndex = this.findAggregateIndex(this.selectParts[0]);
- // add or remove TimescaleDB aggregate functions as needed
- if (aggIndex !== -1) {
- const baseOpts = this.selectParts[0][aggIndex].def.params[0].baseOptions;
- const timescaleOpts = baseOpts.concat(this.selectParts[0][aggIndex].def.params[0].timescaleOptions);
- if (this.datasource.jsonData.timescaledb === true) {
- this.selectParts[0][aggIndex].def.params[0].options = timescaleOpts;
- } else {
- this.selectParts[0][aggIndex].def.params[0].options = baseOpts;
- }
- }
- }
- updateProjection() {
- this.selectParts = map(this.target.select, (parts: any) => {
- return map(parts, sqlPart.create).filter((n) => n);
- });
- this.timescaleAggCheck();
- this.whereParts = map(this.target.where, sqlPart.create).filter((n) => n);
- this.groupParts = map(this.target.group, sqlPart.create).filter((n) => n);
- }
- updatePersistedParts() {
- this.target.select = map(this.selectParts, (selectParts) => {
- return map(selectParts, (part: any) => {
- return { type: part.def.type, datatype: part.datatype, params: part.params };
- });
- });
- this.timescaleAggCheck();
- this.target.where = map(this.whereParts, (part: any) => {
- return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params };
- });
- this.target.group = map(this.groupParts, (part: any) => {
- return { type: part.def.type, datatype: part.datatype, params: part.params };
- });
- }
- buildSelectMenu() {
- this.selectMenu = [];
- const aggregates = {
- text: 'Aggregate Functions',
- value: 'aggregate',
- submenu: [
- { text: 'Average', value: 'avg' },
- { text: 'Count', value: 'count' },
- { text: 'Maximum', value: 'max' },
- { text: 'Minimum', value: 'min' },
- { text: 'Sum', value: 'sum' },
- { text: 'Standard deviation', value: 'stddev' },
- { text: 'Variance', value: 'variance' },
- ],
- };
- // first and last aggregate are timescaledb specific
- if (this.datasource.jsonData.timescaledb === true) {
- aggregates.submenu.push({ text: 'First', value: 'first' });
- aggregates.submenu.push({ text: 'Last', value: 'last' });
- }
- this.selectMenu.push(aggregates);
- // ordered set aggregates require postgres 9.4+
- if (this.datasource.jsonData.postgresVersion >= 904) {
- const aggregates2 = {
- text: 'Ordered-Set Aggregate Functions',
- value: 'percentile',
- submenu: [
- { text: 'Percentile (continuous)', value: 'percentile_cont' },
- { text: 'Percentile (discrete)', value: 'percentile_disc' },
- ],
- };
- this.selectMenu.push(aggregates2);
- }
- const windows = {
- text: 'Window Functions',
- value: 'window',
- submenu: [
- { text: 'Delta', value: 'delta' },
- { text: 'Increase', value: 'increase' },
- { text: 'Rate', value: 'rate' },
- { text: 'Sum', value: 'sum' },
- { text: 'Moving Average', value: 'avg', type: 'moving_window' },
- ],
- };
- this.selectMenu.push(windows);
- this.selectMenu.push({ text: 'Alias', value: 'alias' });
- this.selectMenu.push({ text: 'Column', value: 'column' });
- }
- toggleEditorMode() {
- if (this.target.rawQuery) {
- appEvents.publish(
- new ShowConfirmModalEvent({
- title: 'Warning',
- text2: 'Switching to query builder may overwrite your raw SQL.',
- icon: 'exclamation-triangle',
- yesText: 'Switch',
- onConfirm: () => {
- // This could be called from React, so wrap in $evalAsync.
- // Will then either run as part of the current digest cycle or trigger a new one.
- this.$scope.$evalAsync(() => {
- this.target.rawQuery = !this.target.rawQuery;
- });
- },
- })
- );
- } else {
- // This could be called from React, so wrap in $evalAsync.
- // Will then either run as part of the current digest cycle or trigger a new one.
- this.$scope.$evalAsync(() => {
- this.target.rawQuery = !this.target.rawQuery;
- });
- }
- }
- resetPlusButton(button: { html: any; value: any; type: any; fake: any }) {
- const plusButton = this.uiSegmentSrv.newPlusButton();
- button.html = plusButton.html;
- button.value = plusButton.value;
- button.type = plusButton.type;
- button.fake = plusButton.fake;
- }
- getTableSegments() {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildTableQuery())
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- }
- tableChanged() {
- this.target.table = this.tableSegment.value;
- this.target.where = [];
- this.target.group = [];
- this.updateProjection();
- const segment = this.uiSegmentSrv.newSegment('none');
- this.metricColumnSegment.html = segment.html;
- this.metricColumnSegment.value = segment.value;
- this.target.metricColumn = 'none';
- const task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then((result: any) => {
- // check if time column is still valid
- if (result.length > 0 && !find(result, (r: any) => r.text === this.target.timeColumn)) {
- const segment = this.uiSegmentSrv.newSegment(result[0].text);
- this.timeColumnSegment.html = segment.html;
- this.timeColumnSegment.value = segment.value;
- }
- return this.timeColumnChanged(false);
- });
- const task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then((result: any) => {
- if (result.length > 0) {
- this.target.select = [[{ type: 'column', params: [result[0].text] }]];
- this.updateProjection();
- }
- });
- Promise.all([task1, task2]).then(() => {
- this.updateRawSqlAndRefresh();
- });
- }
- getTimeColumnSegments() {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery('time'))
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- }
- timeColumnChanged(refresh?: boolean) {
- this.target.timeColumn = this.timeColumnSegment.value;
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn))
- .then((result: any) => {
- if (result.length === 1) {
- if (this.target.timeColumnType !== result[0].text) {
- this.target.timeColumnType = result[0].text;
- }
- let partModel;
- if (this.queryModel.hasUnixEpochTimecolumn()) {
- partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] });
- } else {
- partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] });
- }
- if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
- // replace current macro
- this.whereParts[0] = partModel;
- } else {
- this.whereParts.splice(0, 0, partModel);
- }
- }
- this.updatePersistedParts();
- if (refresh !== false) {
- this.updateRawSqlAndRefresh();
- }
- });
- }
- getMetricColumnSegments() {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery('metric'))
- .then(this.transformToSegments({ addNone: true }))
- .catch(this.handleQueryError.bind(this));
- }
- metricColumnChanged() {
- this.target.metricColumn = this.metricColumnSegment.value;
- this.updateRawSqlAndRefresh();
- }
- onDataReceived(dataList: any) {
- this.lastQueryError = undefined;
- this.lastQueryMeta = dataList[0]?.meta;
- }
- onDataError(err: any) {
- if (err.data && err.data.results) {
- const queryRes = err.data.results[this.target.refId];
- if (queryRes) {
- this.lastQueryError = queryRes.error;
- }
- }
- }
- transformToSegments(config: { addNone?: any; addTemplateVars?: any; templateQuoter?: any }) {
- return (results: any) => {
- const segments = map(results, (segment) => {
- return this.uiSegmentSrv.newSegment({
- value: segment.text,
- expandable: segment.expandable,
- });
- });
- if (config.addTemplateVars) {
- for (const variable of this.templateSrv.getVariables()) {
- let value;
- value = '$' + variable.name;
- if (config.templateQuoter && (variable as unknown as VariableWithMultiSupport).multi === false) {
- value = config.templateQuoter(value);
- }
- segments.unshift(
- this.uiSegmentSrv.newSegment({
- type: 'template',
- value: value,
- expandable: true,
- })
- );
- }
- }
- if (config.addNone) {
- segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true }));
- }
- return segments;
- };
- }
- findAggregateIndex(selectParts: any) {
- return findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile');
- }
- findWindowIndex(selectParts: any) {
- return findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window');
- }
- addSelectPart(selectParts: any[], item: { value: any }, subItem: { type: any; value: any }) {
- let partType = item.value;
- if (subItem && subItem.type) {
- partType = subItem.type;
- }
- let partModel = sqlPart.create({ type: partType });
- if (subItem) {
- partModel.params[0] = subItem.value;
- }
- let addAlias = false;
- switch (partType) {
- case 'column':
- const parts = map(selectParts, (part: any) => {
- return sqlPart.create({ type: part.def.type, params: clone(part.params) });
- });
- this.selectParts.push(parts);
- break;
- case 'percentile':
- case 'aggregate':
- // add group by if no group by yet
- if (this.target.group.length === 0) {
- this.addGroup('time', '$__interval');
- }
- const aggIndex = this.findAggregateIndex(selectParts);
- if (aggIndex !== -1) {
- // replace current aggregation
- selectParts[aggIndex] = partModel;
- } else {
- selectParts.splice(1, 0, partModel);
- }
- if (!find(selectParts, (p: any) => p.def.type === 'alias')) {
- addAlias = true;
- }
- break;
- case 'moving_window':
- case 'window':
- const windowIndex = this.findWindowIndex(selectParts);
- if (windowIndex !== -1) {
- // replace current window function
- selectParts[windowIndex] = partModel;
- } else {
- const aggIndex = this.findAggregateIndex(selectParts);
- if (aggIndex !== -1) {
- selectParts.splice(aggIndex + 1, 0, partModel);
- } else {
- selectParts.splice(1, 0, partModel);
- }
- }
- if (!find(selectParts, (p: any) => p.def.type === 'alias')) {
- addAlias = true;
- }
- break;
- case 'alias':
- addAlias = true;
- break;
- }
- if (addAlias) {
- // set initial alias name to column name
- partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] });
- if (selectParts[selectParts.length - 1].def.type === 'alias') {
- selectParts[selectParts.length - 1] = partModel;
- } else {
- selectParts.push(partModel);
- }
- }
- this.updatePersistedParts();
- this.updateRawSqlAndRefresh();
- }
- removeSelectPart(selectParts: any, part: { def: { type: string } }) {
- if (part.def.type === 'column') {
- // remove all parts of column unless its last column
- if (this.selectParts.length > 1) {
- const modelsIndex = indexOf(this.selectParts, selectParts);
- this.selectParts.splice(modelsIndex, 1);
- }
- } else {
- const partIndex = indexOf(selectParts, part);
- selectParts.splice(partIndex, 1);
- }
- this.updatePersistedParts();
- }
- handleSelectPartEvent(selectParts: any, part: { def: any }, evt: { name: any }) {
- switch (evt.name) {
- case 'get-param-options': {
- switch (part.def.type) {
- case 'aggregate':
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildAggregateQuery())
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- case 'column':
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery('value'))
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- }
- }
- case 'part-param-changed': {
- this.updatePersistedParts();
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'action': {
- this.removeSelectPart(selectParts, part);
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'get-part-actions': {
- return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]);
- }
- }
- }
- handleGroupPartEvent(part: any, index: any, evt: { name: any }) {
- switch (evt.name) {
- case 'get-param-options': {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery())
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- }
- case 'part-param-changed': {
- this.updatePersistedParts();
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'action': {
- this.removeGroup(part, index);
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'get-part-actions': {
- return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]);
- }
- }
- }
- addGroup(partType: string, value: string) {
- let params = [value];
- if (partType === 'time') {
- params = ['$__interval', 'none'];
- }
- const partModel = sqlPart.create({ type: partType, params: params });
- if (partType === 'time') {
- // put timeGroup at start
- this.groupParts.splice(0, 0, partModel);
- } else {
- this.groupParts.push(partModel);
- }
- // add aggregates when adding group by
- for (const selectParts of this.selectParts) {
- if (!selectParts.some((part) => part.def.type === 'aggregate')) {
- const aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] });
- selectParts.splice(1, 0, aggregate);
- if (!selectParts.some((part) => part.def.type === 'alias')) {
- const alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] });
- selectParts.push(alias);
- }
- }
- }
- this.updatePersistedParts();
- }
- removeGroup(part: { def: { type: string } }, index: number) {
- if (part.def.type === 'time') {
- // remove aggregations
- this.selectParts = map(this.selectParts, (s: any) => {
- return filter(s, (part: any) => {
- if (part.def.type === 'aggregate' || part.def.type === 'percentile') {
- return false;
- }
- return true;
- });
- });
- }
- this.groupParts.splice(index, 1);
- this.updatePersistedParts();
- }
- handleWherePartEvent(whereParts: any, part: any, evt: any, index: any) {
- switch (evt.name) {
- case 'get-param-options': {
- switch (evt.param.name) {
- case 'left':
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery())
- .then(this.transformToSegments({}))
- .catch(this.handleQueryError.bind(this));
- case 'right':
- if (['int4', 'int8', 'float4', 'float8', 'timestamp', 'timestamptz'].indexOf(part.datatype) > -1) {
- // don't do value lookups for numerical fields
- return Promise.resolve([]);
- } else {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildValueQuery(part.params[0]))
- .then(
- this.transformToSegments({
- addTemplateVars: true,
- templateQuoter: (v: string) => {
- return this.queryModel.quoteLiteral(v);
- },
- })
- )
- .catch(this.handleQueryError.bind(this));
- }
- case 'op':
- return Promise.resolve(this.uiSegmentSrv.newOperators(this.metaBuilder.getOperators(part.datatype)));
- default:
- return Promise.resolve([]);
- }
- }
- case 'part-param-changed': {
- this.updatePersistedParts();
- this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(part.params[0])).then((d: any) => {
- if (d.length === 1) {
- part.datatype = d[0].text;
- }
- });
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'action': {
- // remove element
- whereParts.splice(index, 1);
- this.updatePersistedParts();
- this.updateRawSqlAndRefresh();
- break;
- }
- case 'get-part-actions': {
- return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]);
- }
- }
- }
- getWhereOptions() {
- const options = [];
- if (this.queryModel.hasUnixEpochTimecolumn()) {
- options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' }));
- } else {
- options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__timeFilter' }));
- }
- options.push(this.uiSegmentSrv.newSegment({ type: 'expression', value: 'Expression' }));
- return Promise.resolve(options);
- }
- addWhereAction(part: any, index: any) {
- switch (this.whereAdd.type) {
- case 'macro': {
- const partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] });
- if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
- // replace current macro
- this.whereParts[0] = partModel;
- } else {
- this.whereParts.splice(0, 0, partModel);
- }
- break;
- }
- default: {
- this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] }));
- }
- }
- this.updatePersistedParts();
- this.resetPlusButton(this.whereAdd);
- this.updateRawSqlAndRefresh();
- }
- getGroupOptions() {
- return this.datasource
- .metricFindQuery(this.metaBuilder.buildColumnQuery('group'))
- .then((tags: any) => {
- const options = [];
- if (!this.queryModel.hasTimeGroup()) {
- options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' }));
- }
- for (const tag of tags) {
- options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text }));
- }
- return options;
- })
- .catch(this.handleQueryError.bind(this));
- }
- addGroupAction() {
- this.addGroup(this.groupAdd.type, this.groupAdd.value);
- this.resetPlusButton(this.groupAdd);
- this.updateRawSqlAndRefresh();
- }
- handleQueryError(err: any): any[] {
- this.error = err.message || 'Failed to issue metric query';
- return [];
- }
- }
|