mysql query editor - angular to react (#50343)

mysql conversion to react

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
pull/51437/head^2
Scott Lepper 3 years ago committed by GitHub
parent 6e1e4a4215
commit 53933972b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 114
      .betterer.results
  2. 2
      pkg/services/dashboards/store_mock.go
  3. 14
      public/app/features/plugins/sql/components/visual-query-builder/SQLOrderByRow.tsx
  4. 10
      public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx
  5. 10
      public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx
  6. 1
      public/app/features/plugins/sql/datasource/SqlDatasource.ts
  7. 17
      public/app/features/plugins/sql/types.ts
  8. 2
      public/app/plugins/datasource/mssql/datasource.ts
  9. 112
      public/app/plugins/datasource/mysql/MySqlDatasource.ts
  10. 61
      public/app/plugins/datasource/mysql/MySqlQueryModel.ts
  11. 27
      public/app/plugins/datasource/mysql/MySqlResponseParser.ts
  12. 212
      public/app/plugins/datasource/mysql/datasource.ts
  13. 91
      public/app/plugins/datasource/mysql/fields.ts
  14. 20
      public/app/plugins/datasource/mysql/functions.ts
  15. 142
      public/app/plugins/datasource/mysql/meta_query.ts
  16. 43
      public/app/plugins/datasource/mysql/module.ts
  17. 60
      public/app/plugins/datasource/mysql/mySqlMetaQuery.ts
  18. 236
      public/app/plugins/datasource/mysql/mysql_query_model.ts
  19. 54
      public/app/plugins/datasource/mysql/partials/annotations.editor.html
  20. 190
      public/app/plugins/datasource/mysql/partials/query.editor.html
  21. 647
      public/app/plugins/datasource/mysql/query_ctrl.ts
  22. 77
      public/app/plugins/datasource/mysql/response_parser.ts
  23. 64
      public/app/plugins/datasource/mysql/specs/datasource.test.ts
  24. 281
      public/app/plugins/datasource/mysql/sqlCompletionProvider.ts
  25. 86
      public/app/plugins/datasource/mysql/sql_part.ts
  26. 23
      public/app/plugins/datasource/mysql/types.ts

@ -7778,125 +7778,15 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mysql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
],
"public/app/plugins/datasource/mysql/meta_query.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/mysql_query_model.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"]
],
"public/app/plugins/datasource/mysql/query_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Do not use any type assertions.", "24"],
[0, 0, 0, "Do not use any type assertions.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"],
[0, 0, 0, "Unexpected any. Specify a different type.", "32"],
[0, 0, 0, "Unexpected any. Specify a different type.", "33"],
[0, 0, 0, "Unexpected any. Specify a different type.", "34"],
[0, 0, 0, "Unexpected any. Specify a different type.", "35"],
[0, 0, 0, "Unexpected any. Specify a different type.", "36"],
[0, 0, 0, "Unexpected any. Specify a different type.", "37"],
[0, 0, 0, "Unexpected any. Specify a different type.", "38"],
[0, 0, 0, "Unexpected any. Specify a different type.", "39"],
[0, 0, 0, "Unexpected any. Specify a different type.", "40"],
[0, 0, 0, "Unexpected any. Specify a different type.", "41"],
[0, 0, 0, "Unexpected any. Specify a different type.", "42"],
[0, 0, 0, "Unexpected any. Specify a different type.", "43"],
[0, 0, 0, "Unexpected any. Specify a different type.", "44"],
[0, 0, 0, "Unexpected any. Specify a different type.", "45"],
[0, 0, 0, "Unexpected any. Specify a different type.", "46"],
[0, 0, 0, "Unexpected any. Specify a different type.", "47"],
[0, 0, 0, "Unexpected any. Specify a different type.", "48"],
[0, 0, 0, "Unexpected any. Specify a different type.", "49"],
[0, 0, 0, "Unexpected any. Specify a different type.", "50"],
[0, 0, 0, "Unexpected any. Specify a different type.", "51"],
[0, 0, 0, "Unexpected any. Specify a different type.", "52"],
[0, 0, 0, "Unexpected any. Specify a different type.", "53"],
[0, 0, 0, "Unexpected any. Specify a different type.", "54"]
],
"public/app/plugins/datasource/mysql/response_parser.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mysql/specs/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/sql_part.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/mysql/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/datasource/opentsdb/datasource.d.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

@ -435,7 +435,7 @@ func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dash
return r0, r1
}
func (_m *FakeDashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error{
func (_m *FakeDashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error {
ret := _m.Called(ctx, userID)
var r0 error

@ -20,16 +20,18 @@ export function SQLOrderByRow({ fields, query, onQueryChange, db }: SQLOrderByRo
let columnsWithIndices: SelectableValue[] = [];
if (fields) {
const options = query.sql?.columns?.map((c, i) => {
const value = c.name ? `${c.name}(${c.parameters?.map((p) => p.name)})` : c.parameters?.map((p) => p.name);
return {
value,
label: `${i + 1} - ${value}`,
};
});
columnsWithIndices = [
{
value: '',
label: 'Selected columns',
options: query.sql?.columns?.map((c, i) => ({
value: i + 1,
label: c.name
? `${i + 1} - ${c.name}(${c.parameters?.map((p) => `${p.name}`)})`
: c.parameters?.map((p) => `${i + 1} - ${p.name}`),
})),
options,
expanded: true,
},
...fields,

@ -1,6 +1,7 @@
import React from 'react';
import { useAsync } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { SelectableValue, toOption } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
@ -18,5 +19,10 @@ interface SQLSelectRowProps {
export function SQLSelectRow({ fields, query, onQueryChange, db }: SQLSelectRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
return <SelectRow columns={fields} sql={query.sql!} onSqlChange={onSqlChange} />;
const state = useAsync(async () => {
const functions = await db.functions();
return functions.map((f) => toOption(f.name));
}, [db]);
return <SelectRow columns={fields} sql={query.sql!} functions={state.value} onSqlChange={onSqlChange} />;
}

@ -6,7 +6,6 @@ import { SelectableValue, toOption } from '@grafana/data';
import { EditorField, Stack } from '@grafana/experimental';
import { Button, Select, useStyles2 } from '@grafana/ui';
import { AGGREGATE_FNS } from '../../constants';
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
import { SQLExpression } from '../../types';
import { createFunctionField } from '../../utils/sql.utils';
@ -15,11 +14,12 @@ interface SelectRowProps {
sql: SQLExpression;
onSqlChange: (sql: SQLExpression) => void;
columns?: Array<SelectableValue<string>>;
functions?: Array<SelectableValue<string>>;
}
const asteriskValue = { label: '*', value: '*' };
export function SelectRow({ sql, columns, onSqlChange }: SelectRowProps) {
export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowProps) {
const styles = useStyles2(getStyles);
const columnsWithAsterisk = [asteriskValue, ...(columns || [])];
@ -101,7 +101,7 @@ export function SelectRow({ sql, columns, onSqlChange }: SelectRowProps) {
isClearable
menuShouldPortal
allowCustomValue
options={aggregateFnOptions}
options={functions}
onChange={onAggregationChange(item, index)}
/>
</EditorField>
@ -133,10 +133,6 @@ const getStyles = () => {
return { addButton: css({ alignSelf: 'flex-start' }) };
};
const aggregateFnOptions = AGGREGATE_FNS.map((v: { id: string; name: string; description: string }) =>
toOption(v.name)
);
function getColumnValue({ parameters }: QueryEditorFunctionExpression): SelectableValue<string> | null {
const column = parameters?.find((p) => p.type === QueryEditorExpressionType.FunctionParameter);
if (column?.name) {

@ -204,6 +204,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
interface RunSQLOptions extends MetricFindQueryOptions {
refId?: string;
}
interface MetricFindQueryOptions extends SearchFilterOptions {
range?: TimeRange;
}

@ -18,6 +18,15 @@ import {
QueryEditorPropertyExpression,
} from './expressions';
export interface SqlQueryForInterpolation {
dataset?: string;
alias?: string;
format?: QueryFormat;
rawSql?: string;
refId: string;
hide?: boolean;
}
export interface SQLConnectionLimits {
maxOpenConns: number;
maxIdleConns: number;
@ -112,6 +121,13 @@ export interface SQLSelectableValue extends SelectableValue {
type?: string;
raqbFieldType?: RAQBFieldTypes;
}
export interface Aggregate {
id: string;
name: string;
description?: string;
}
export interface DB {
init?: (datasourceId?: string) => Promise<boolean>;
datasets: () => Promise<string[]>;
@ -123,6 +139,7 @@ export interface DB {
lookup: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
getSqlCompletionProvider: () => LanguageCompletionProvider;
toRawSql?: (query: SQLQuery) => string;
functions: () => Promise<Aggregate[]>;
}
export interface QueryEditorProps {

@ -1,6 +1,7 @@
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { LanguageCompletionProvider } from '@grafana/experimental';
import { TemplateSrv } from '@grafana/runtime';
import { AGGREGATE_FNS } from 'app/features/plugins/sql/constants';
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
import { DB, ResponseParser, SQLQuery, SQLSelectableValue } from 'app/features/plugins/sql/types';
@ -92,6 +93,7 @@ export class MssqlDatasource extends SqlDatasource {
}
}
},
functions: async () => AGGREGATE_FNS,
};
}
}

@ -0,0 +1,112 @@
import { DataSourceInstanceSettings, ScopedVars, TimeRange } from '@grafana/data';
import { CompletionItemKind, LanguageCompletionProvider } from '@grafana/experimental';
import { TemplateSrv } from '@grafana/runtime';
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
import { DB, ResponseParser, SQLQuery } from 'app/features/plugins/sql/types';
import MySQLQueryModel from './MySqlQueryModel';
import MySqlResponseParser from './MySqlResponseParser';
import { mapFieldsToTypes } from './fields';
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
import { fetchColumns, fetchTables, getFunctions, getSqlCompletionProvider } from './sqlCompletionProvider';
import { MySQLOptions } from './types';
export class MySqlDatasource extends SqlDatasource {
responseParser: MySqlResponseParser;
completionProvider: LanguageCompletionProvider | undefined;
constructor(private instanceSettings: DataSourceInstanceSettings<MySQLOptions>) {
super(instanceSettings);
this.responseParser = new MySqlResponseParser();
this.completionProvider = undefined;
}
getQueryModel(target?: Partial<SQLQuery>, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): MySQLQueryModel {
return new MySQLQueryModel(target!, templateSrv, scopedVars);
}
getResponseParser(): ResponseParser {
return this.responseParser;
}
getSqlCompletionProvider(db: DB): LanguageCompletionProvider {
if (this.completionProvider !== undefined) {
return this.completionProvider;
}
const args = {
getColumns: { current: (query: SQLQuery) => fetchColumns(db, query) },
getTables: { current: (dataset?: string) => fetchTables(db, { dataset }) },
fetchMeta: { current: (path?: string) => this.fetchMeta(path) },
getFunctions: { current: () => getFunctions() },
};
this.completionProvider = getSqlCompletionProvider(args);
return this.completionProvider;
}
async fetchDatasets(): Promise<string[]> {
const datasets = await this.runSql<string[]>(showDatabases(), { refId: 'datasets' });
return datasets.map((t) => t[0]);
}
async fetchTables(dataset?: string): Promise<string[]> {
const tables = await this.runSql<string[]>(buildTableQuery(dataset), { refId: 'tables' });
return tables.map((t) => t[0]);
}
async fetchFields(query: Partial<SQLQuery>) {
if (!query.dataset || !query.table) {
return [];
}
const queryString = buildColumnQuery(this.getQueryModel(query), query.table!);
const frame = await this.runSql<string[]>(queryString, { refId: 'fields' });
const fields = frame.map((f) => ({ name: f[0], text: f[0], value: f[0], type: f[1], label: f[0] }));
return mapFieldsToTypes(fields);
}
async fetchMeta(path?: string) {
const defaultDB = this.instanceSettings.jsonData.database;
path = path?.trim();
if (!path && defaultDB) {
const tables = await this.fetchTables(defaultDB);
return tables.map((t) => ({ name: t, completion: t, kind: CompletionItemKind.Class }));
} else if (!path) {
const datasets = await this.fetchDatasets();
return datasets.map((d) => ({ name: d, completion: `${d}.`, kind: CompletionItemKind.Module }));
} else {
const parts = path.split('.').filter((s: string) => s);
if (parts.length > 2) {
return [];
}
if (parts.length === 1 && !defaultDB) {
const tables = await this.fetchTables(parts[0]);
return tables.map((t) => ({ name: t, completion: t, kind: CompletionItemKind.Class }));
} else if (parts.length === 1 && defaultDB) {
const fields = await this.fetchFields({ dataset: defaultDB, table: parts[0] });
return fields.map((t) => ({ name: t.value, completion: t.value, kind: CompletionItemKind.Field }));
} else if (parts.length === 2 && !defaultDB) {
const fields = await this.fetchFields({ dataset: parts[0], table: parts[1] });
return fields.map((t) => ({ name: t.value, completion: t.value, kind: CompletionItemKind.Field }));
} else {
return [];
}
}
}
getDB(): DB {
if (this.db !== undefined) {
return this.db;
}
return {
datasets: () => this.fetchDatasets(),
tables: (dataset?: string) => this.fetchTables(dataset),
fields: (query: SQLQuery) => this.fetchFields(query),
validateQuery: (query: SQLQuery, range?: TimeRange) =>
Promise.resolve({ query, error: '', isError: false, isValid: true }),
dsID: () => this.id,
lookup: (path?: string) => this.fetchMeta(path),
getSqlCompletionProvider: () => this.getSqlCompletionProvider(this.db),
functions: async () => getFunctions(),
};
}
}

@ -0,0 +1,61 @@
import { map } from 'lodash';
import { ScopedVars } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { MySQLQuery } from './types';
export default class MySQLQueryModel {
target: Partial<MySQLQuery>;
templateSrv?: TemplateSrv;
scopedVars?: ScopedVars;
constructor(target: Partial<MySQLQuery>, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.target = target;
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
}
// remove identifier quoting from identifier to use in metadata queries
unquoteIdentifier(value: string) {
if (value[0] === '"' && value[value.length - 1] === '"') {
return value.substring(1, value.length - 1).replace(/""/g, '"');
} else {
return value;
}
}
quoteIdentifier(value: string) {
return '"' + value.replace(/"/g, '""') + '"';
}
quoteLiteral(value: string) {
return "'" + value.replace(/'/g, "''") + "'";
}
escapeLiteral(value: string) {
return String(value).replace(/'/g, "''");
}
format = (value: string, variable: { multi: boolean; includeAll: boolean }) => {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return this.escapeLiteral(value);
}
if (typeof value === 'string') {
return this.quoteLiteral(value);
}
const escapedValues = map(value, this.quoteLiteral);
return escapedValues.join(',');
};
interpolate() {
return this.templateSrv!.replace(this.target.rawSql, this.scopedVars, this.format);
}
getDatabase() {
return this.target.dataset;
}
}

@ -0,0 +1,27 @@
import { uniqBy } from 'lodash';
import { DataFrame, MetricFindValue } from '@grafana/data';
export default class ResponseParser {
transformMetricFindResponse(frame: DataFrame): MetricFindValue[] {
const values: MetricFindValue[] = [];
const textField = frame.fields.find((f) => f.name === '__text');
const valueField = frame.fields.find((f) => f.name === '__value');
if (textField && valueField) {
for (let i = 0; i < textField.values.length; i++) {
values.push({ text: '' + textField.values.get(i), value: '' + valueField.values.get(i) });
}
} else {
values.push(
...frame.fields
.flatMap((f) => f.values.toArray())
.map((v) => ({
text: v,
}))
);
}
return uniqBy(values, 'text');
}
}

@ -1,212 +0,0 @@
import { map as _map } from 'lodash';
import { lastValueFrom, of } from 'rxjs';
import { catchError, map, mapTo } from 'rxjs/operators';
import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data';
import { BackendDataSourceResponse, DataSourceWithBackend, FetchResponse, getBackendSrv } from '@grafana/runtime';
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import MySQLQueryModel from 'app/plugins/datasource/mysql/mysql_query_model';
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
import ResponseParser from './response_parser';
import { MySQLOptions, MySQLQuery, MysqlQueryForInterpolation } from './types';
export class MysqlDatasource extends DataSourceWithBackend<MySQLQuery, MySQLOptions> {
id: any;
name: any;
responseParser: ResponseParser;
queryModel: MySQLQueryModel;
interval: string;
constructor(
instanceSettings: DataSourceInstanceSettings<MySQLOptions>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser();
this.queryModel = new MySQLQueryModel({});
const settingsData = instanceSettings.jsonData || ({} as MySQLOptions);
this.interval = settingsData.timeInterval || '1m';
}
interpolateVariable = (value: string | string[] | number, variable: any) => {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
const result = this.queryModel.quoteLiteral(value);
return result;
} else {
return value;
}
}
if (typeof value === 'number') {
return value;
}
const quotedValues = _map(value, (v: any) => {
return this.queryModel.quoteLiteral(v);
});
return quotedValues.join(',');
};
interpolateVariablesInQueries(
queries: MysqlQueryForInterpolation[],
scopedVars: ScopedVars
): MysqlQueryForInterpolation[] {
let expandedQueries = queries;
if (queries && queries.length > 0) {
expandedQueries = queries.map((query) => {
const expandedQuery = {
...query,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true,
};
return expandedQuery;
});
}
return expandedQueries;
}
filterQuery(query: MySQLQuery): boolean {
return !query.hide;
}
applyTemplateVariables(target: MySQLQuery, scopedVars: ScopedVars): Record<string, any> {
const queryModel = new MySQLQueryModel(target, this.templateSrv, scopedVars);
return {
refId: target.refId,
datasource: this.getRef(),
rawSql: queryModel.render(this.interpolateVariable as any),
format: target.format,
};
}
async annotationQuery(options: any): Promise<AnnotationEvent[]> {
if (!options.annotation.rawQuery) {
return Promise.reject({
message: 'Query missing in annotation definition',
});
}
const query = {
refId: options.annotation.name,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(options.annotation.rawQuery, options.scopedVars, this.interpolateVariable),
format: 'table',
};
return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: [query],
},
requestId: options.annotation.name,
})
.pipe(
map(
async (res: FetchResponse<BackendDataSourceResponse>) =>
await this.responseParser.transformAnnotationResponse(options, res.data)
)
)
);
}
metricFindQuery(query: string, optionalOptions: any): Promise<MetricFindValue[]> {
let refId = 'tempvar';
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
refId = optionalOptions.variable.name;
}
const rawSql = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '%', options: optionalOptions }),
this.interpolateVariable
);
const interpolatedQuery = {
refId: refId,
datasource: this.getRef(),
rawSql,
format: 'table',
};
const range = optionalOptions?.range;
return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
data: {
from: range?.from?.valueOf()?.toString(),
to: range?.to?.valueOf()?.toString(),
queries: [interpolatedQuery],
},
requestId: refId,
})
.pipe(
map((rsp) => {
return this.responseParser.transformMetricFindResponse(rsp);
}),
catchError((err) => {
return of([]);
})
)
);
}
testDatasource(): Promise<any> {
return lastValueFrom(
getBackendSrv()
.fetch({
url: '/api/ds/query',
method: 'POST',
data: {
from: '5m',
to: 'now',
queries: [
{
refId: 'A',
intervalMs: 1,
maxDataPoints: 1,
datasource: this.getRef(),
rawSql: 'SELECT 1',
format: 'table',
},
],
},
})
.pipe(
mapTo({ status: 'success', message: 'Database Connection OK' }),
catchError((err) => {
return of(toTestingStatus(err));
})
)
);
}
targetContainsTemplate(target: any) {
let rawSql = '';
if (target.rawQuery) {
rawSql = target.rawSql;
} else {
const query = new MySQLQueryModel(target);
rawSql = query.buildQuery();
}
rawSql = rawSql.replace('$__', '');
return this.templateSrv.containsTemplate(rawSql);
}
}

@ -0,0 +1,91 @@
import { RAQBFieldTypes, SQLSelectableValue } from 'app/features/plugins/sql/types';
export function mapFieldsToTypes(columns: SQLSelectableValue[]) {
const fields: SQLSelectableValue[] = [];
for (const col of columns) {
let type: RAQBFieldTypes = 'text';
switch (col.type?.toUpperCase()) {
case 'BOOLEAN':
case 'BOOL': {
type = 'boolean';
break;
}
case 'BYTES':
case 'VARCHAR': {
type = 'text';
break;
}
case 'FLOAT':
case 'FLOAT64':
case 'INT':
case 'INTEGER':
case 'INT64':
case 'NUMERIC':
case 'BIGNUMERIC': {
type = 'number';
break;
}
case 'DATE': {
type = 'date';
break;
}
case 'DATETIME': {
type = 'datetime';
break;
}
case 'TIME': {
type = 'time';
break;
}
case 'TIMESTAMP': {
type = 'datetime';
break;
}
case 'GEOGRAPHY': {
type = 'text';
break;
}
default:
break;
}
fields.push({ ...col, raqbFieldType: type, icon: mapColumnTypeToIcon(col.type!.toUpperCase()) });
}
return fields;
}
export function mapColumnTypeToIcon(type: string) {
switch (type) {
case 'TIME':
case 'DATETIME':
case 'TIMESTAMP':
return 'clock-nine';
case 'BOOLEAN':
return 'toggle-off';
case 'INTEGER':
case 'FLOAT':
case 'FLOAT64':
case 'INT':
case 'SMALLINT':
case 'BIGINT':
case 'TINYINT':
case 'BYTEINT':
case 'INT64':
case 'NUMERIC':
case 'DECIMAL':
return 'calculator-alt';
case 'CHAR':
case 'VARCHAR':
case 'STRING':
case 'BYTES':
case 'TEXT':
case 'TINYTEXT':
case 'MEDIUMTEXT':
case 'LONGTEXT':
return 'text';
case 'GEOGRAPHY':
return 'map';
default:
return undefined;
}
}

@ -0,0 +1,20 @@
export const FUNCTIONS = [
{
id: 'STDDEV',
name: 'STDDEV',
description: `STDDEV(
expression
)
Returns the standard deviation of non-NULL input values, or NaN if the input contains a NaN.`,
},
{
id: 'VARIANCE',
name: 'VARIANCE',
description: `VARIANCE(
expression
)
Returns the variance of non-NULL input values, or NaN if the input contains a NaN.`,
},
];

@ -1,142 +0,0 @@
export class MysqlMetaQuery {
constructor(private target: any, private queryModel: any) {}
getOperators(datatype: string) {
switch (datatype) {
case 'double':
case 'float': {
return ['=', '!=', '<', '<=', '>', '>='];
}
case 'text':
case 'tinytext':
case 'mediumtext':
case 'longtext':
case 'varchar':
case 'char': {
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE'];
}
default: {
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN'];
}
}
}
// quote identifier as literal to use in metadata queries
quoteIdentAsLiteral(value: string) {
return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value));
}
findMetricTable() {
// query that returns first table found that has a timestamp(tz) column and a float column
const query = `
SELECT
table_name as table_name,
( SELECT
column_name as column_name
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
c.data_type IN ('timestamp', 'datetime')
ORDER BY ordinal_position LIMIT 1
) AS time_column,
( SELECT
column_name AS column_name
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
c.data_type IN('float', 'int', 'bigint')
ORDER BY ordinal_position LIMIT 1
) AS value_column
FROM information_schema.tables t
WHERE
t.table_schema = database() AND
EXISTS
( SELECT 1
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
c.data_type IN ('timestamp', 'datetime')
) AND
EXISTS
( SELECT 1
FROM information_schema.columns c
WHERE
c.table_schema = t.table_schema AND
c.table_name = t.table_name AND
c.data_type IN('float', 'int', 'bigint')
)
LIMIT 1
;`;
return query;
}
buildTableConstraint(table: string) {
let query = '';
// check for schema qualified table
if (table.includes('.')) {
const parts = table.split('.');
query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]);
query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]);
return query;
} else {
query = 'table_schema = database() AND table_name = ' + this.quoteIdentAsLiteral(table);
return query;
}
}
buildTableQuery() {
return 'SELECT table_name FROM information_schema.tables WHERE table_schema = database() ORDER BY table_name';
}
buildColumnQuery(type?: string) {
let query = 'SELECT column_name FROM information_schema.columns WHERE ';
query += this.buildTableConstraint(this.target.table);
switch (type) {
case 'time': {
query += " AND data_type IN ('timestamp','datetime','bigint','int','double','float')";
break;
}
case 'metric': {
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
break;
}
case 'value': {
query += " AND data_type IN ('bigint','int','smallint','mediumint','tinyint','double','decimal','float')";
query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
break;
}
case 'group': {
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
break;
}
}
query += ' ORDER BY column_name';
return query;
}
buildValueQuery(column: string) {
let query = 'SELECT DISTINCT QUOTE(' + column + ')';
query += ' FROM ' + this.target.table;
query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')';
query += ' ORDER BY 1 LIMIT 100';
return query;
}
buildDatatypeQuery(column: string) {
let query = `
SELECT data_type
FROM information_schema.columns
WHERE `;
query += ' table_name = ' + this.quoteIdentAsLiteral(this.target.table);
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
return query;
}
}

@ -1,40 +1,11 @@
import { DataSourcePlugin } from '@grafana/data';
import { SqlQueryEditor } from 'app/features/plugins/sql/components/QueryEditor';
import { SQLQuery } from 'app/features/plugins/sql/types';
import { MySqlDatasource } from './MySqlDatasource';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
import { MysqlDatasource } from './datasource';
import { MysqlQueryCtrl } from './query_ctrl';
import { MySQLQuery } from './types';
import { MySQLOptions } from './types';
const defaultQuery = `SELECT
UNIX_TIMESTAMP(<time_column>) as time_sec,
<text_column> as text,
<tags_column> as tags
FROM <table name>
WHERE $__timeFilter(time_column)
ORDER BY <time_column> ASC
LIMIT 100
`;
class MysqlAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
declare annotation: any;
/** @ngInject */
constructor($scope: any) {
this.annotation = $scope.ctrl.annotation;
this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery;
}
}
export {
MysqlDatasource,
MysqlDatasource as Datasource,
MysqlQueryCtrl as QueryCtrl,
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};
export const plugin = new DataSourcePlugin<MysqlDatasource, MySQLQuery>(MysqlDatasource)
.setQueryCtrl(MysqlQueryCtrl)
.setConfigEditor(ConfigurationEditor)
.setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl);
export const plugin = new DataSourcePlugin<MySqlDatasource, SQLQuery, MySQLOptions>(MySqlDatasource)
.setQueryEditor(SqlQueryEditor)
.setConfigEditor(ConfigurationEditor);

@ -0,0 +1,60 @@
import MySQLQueryModel from './MySqlQueryModel';
export function buildTableQuery(dataset?: string) {
const database = dataset !== undefined ? `'${dataset}'` : 'database()';
return `SELECT table_name FROM information_schema.tables WHERE table_schema = ${database} ORDER BY table_name`;
}
export function showDatabases() {
return `SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA`;
}
export function buildColumnQuery(queryModel: MySQLQueryModel, table: string, type?: string, timeColumn?: string) {
let query = 'SELECT column_name, data_type FROM information_schema.columns WHERE ';
query += buildTableConstraint(queryModel, table);
switch (type) {
case 'time': {
query += " AND data_type IN ('timestamp','datetime','bigint','int','double','float')";
break;
}
case 'metric': {
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
break;
}
case 'value': {
query += " AND data_type IN ('bigint','int','smallint','mediumint','tinyint','double','decimal','float')";
query += ' AND column_name <> ' + quoteIdentAsLiteral(queryModel, timeColumn!);
break;
}
case 'group': {
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
break;
}
}
query += ' ORDER BY column_name';
return query;
}
export function buildTableConstraint(queryModel: MySQLQueryModel, table: string) {
let query = '';
// check for schema qualified table
if (table.includes('.')) {
const parts = table.split('.');
query = 'table_schema = ' + quoteIdentAsLiteral(queryModel, parts[0]);
query += ' AND table_name = ' + quoteIdentAsLiteral(queryModel, parts[1]);
return query;
} else {
const database = queryModel.getDatabase() !== undefined ? `'${queryModel.getDatabase()}'` : 'database()';
query = `table_schema = ${database} AND table_name = ` + quoteIdentAsLiteral(queryModel, table);
return query;
}
}
export function quoteIdentAsLiteral(queryModel: MySQLQueryModel, value: string) {
return queryModel.quoteLiteral(queryModel.unquoteIdentifier(value));
}

@ -1,236 +0,0 @@
import { find, map } from 'lodash';
import { ScopedVars } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
export default class MySQLQueryModel {
target: any;
templateSrv: any;
scopedVars: any;
/** @ngInject */
constructor(target: any, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.target = target;
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
target.format = target.format || 'time_series';
target.timeColumn = target.timeColumn || 'time';
target.metricColumn = target.metricColumn || 'none';
target.group = target.group || [];
target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }];
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);
}
// remove identifier quoting from identifier to use in metadata queries
unquoteIdentifier(value: string) {
if (value[0] === '"' && value[value.length - 1] === '"') {
return value.substring(1, value.length - 1).replace(/""/g, '"');
} else {
return value;
}
}
quoteIdentifier(value: string) {
return '"' + value.replace(/"/g, '""') + '"';
}
quoteLiteral(value: string) {
return "'" + value.replace(/'/g, "''") + "'";
}
escapeLiteral(value: any) {
return String(value).replace(/'/g, "''");
}
hasTimeGroup() {
return find(this.target.group, (g: any) => g.type === 'time');
}
hasMetricColumn() {
return this.target.metricColumn !== 'none';
}
interpolateQueryStr(value: string, variable: { multi: any; includeAll: any }, defaultFormatFn: any) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return this.escapeLiteral(value);
}
if (typeof value === 'string') {
return this.quoteLiteral(value);
}
const escapedValues = map(value, this.quoteLiteral);
return escapedValues.join(',');
}
render(interpolate?: boolean) {
const target = this.target;
// new query with no table set yet
if (!this.target.rawQuery && !('table' in this.target)) {
return '';
}
if (!target.rawQuery) {
target.rawSql = this.buildQuery();
}
if (interpolate) {
return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr);
} else {
return target.rawSql;
}
}
hasUnixEpochTimecolumn() {
return ['int', 'bigint', 'double'].indexOf(this.target.timeColumnType) > -1;
}
buildTimeColumn(alias = true) {
const timeGroup = this.hasTimeGroup();
let query;
let macro = '$__timeGroup';
if (timeGroup) {
let args;
if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') {
args = timeGroup.params.join(',');
} else {
args = timeGroup.params[0];
}
if (this.hasUnixEpochTimecolumn()) {
macro = '$__unixEpochGroup';
}
if (alias) {
macro += 'Alias';
}
query = macro + '(' + this.target.timeColumn + ',' + args + ')';
} else {
query = this.target.timeColumn;
if (alias) {
query += ' AS "time"';
}
}
return query;
}
buildMetricColumn() {
if (this.hasMetricColumn()) {
return this.target.metricColumn + ' AS metric';
}
return '';
}
buildValueColumns() {
let query = '';
for (const column of this.target.select) {
query += ',\n ' + this.buildValueColumn(column);
}
return query;
}
buildValueColumn(column: any) {
let query = '';
const columnName: any = find(column, (g: any) => g.type === 'column');
query = columnName.params[0];
const aggregate: any = find(column, (g: any) => g.type === 'aggregate');
if (aggregate) {
const func = aggregate.params[0];
query = func + '(' + query + ')';
}
const alias: any = find(column, (g: any) => g.type === 'alias');
if (alias) {
query += ' AS ' + this.quoteIdentifier(alias.params[0]);
}
return query;
}
buildWhereClause() {
let query = '';
const conditions = map(this.target.where, (tag, index) => {
switch (tag.type) {
case 'macro':
return tag.name + '(' + this.target.timeColumn + ')';
break;
case 'expression':
return tag.params.join(' ');
break;
}
});
if (conditions.length > 0) {
query = '\nWHERE\n ' + conditions.join(' AND\n ');
}
return query;
}
buildGroupClause() {
let query = '';
let groupSection = '';
for (let i = 0; i < this.target.group.length; i++) {
const part = this.target.group[i];
if (i > 0) {
groupSection += ', ';
}
if (part.type === 'time') {
groupSection += '1';
} else {
groupSection += part.params[0];
}
}
if (groupSection.length) {
query = '\nGROUP BY ' + groupSection;
if (this.hasMetricColumn()) {
query += ',2';
}
}
return query;
}
buildQuery() {
let query = 'SELECT';
query += '\n ' + this.buildTimeColumn();
if (this.hasMetricColumn()) {
query += ',\n ' + this.buildMetricColumn();
}
query += this.buildValueColumns();
query += '\nFROM ' + this.target.table;
query += this.buildWhereClause();
query += this.buildGroupClause();
query += '\nORDER BY ' + this.buildTimeColumn(false);
return query;
}
}

@ -1,54 +0,0 @@
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<textarea
rows="10"
class="gf-form-input"
ng-model="ctrl.annotation.rawQuery"
spellcheck="false"
placeholder="query expression"
data-min-length="0"
data-items="100"
ng-model-onblur
ng-change="ctrl.panelCtrl.refresh()"
></textarea>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
Show Help
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info"><h6>Annotation Query Format</h6>
An annotation is an event that is overlaid on top of graphs. The query can have up to four columns per row, the <i>time</i> or <i>time_sec</i> column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> or <i>time_sec</i> for the annotation event time. Use epoch time or any native date data type.
- column with alias: <b>timeend</b> for the annotation event end time. Use epoch time or any native date data type.
- column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'
Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time (or as time_sec)
- $__timeEpoch(column) -&gt; UNIX_TIMESTAMP(column) as time (or as time_sec)
- $__timeFilter(column) -&gt; column BETWEEN FROM_UNIXTIME(1492750877) AND FROM_UNIXTIME(1492750877)
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
- $__unixEpochNanoFilter(column) -&gt; column &gt;= 1494410783152415214 AND column &lt;= 1494497183142514872
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)
- $__timeTo() -&gt; FROM_UNIXTIME(1492750877)
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
- $__unixEpochNanoFrom() -&gt; 1494410783152415214
- $__unixEpochNanoTo() -&gt; 1494497183142514872
</pre>
</div>
</div>

@ -1,190 +0,0 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
<div ng-if="ctrl.target.rawQuery">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql" textarea-label="Query Editor">
</code-editor>
</div>
</div>
</div>
<div ng-if="!ctrl.target.rawQuery">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">FROM</label>
<metric-segment segment="ctrl.tableSegment" get-options="ctrl.getTableSegments()" on-change="ctrl.tableChanged()"></metric-segment>
<label class="gf-form-label query-keyword width-7">Time column</label>
<metric-segment segment="ctrl.timeColumnSegment" get-options="ctrl.getTimeColumnSegments()" on-change="ctrl.timeColumnChanged()"></metric-segment>
<label class="gf-form-label query-keyword width-9">
Metric column
<info-popover mode="right-normal">Column to be used as metric name for the value column.</info-popover>
</label>
<metric-segment segment="ctrl.metricColumnSegment" get-options="ctrl.getMetricColumnSegments()" on-change="ctrl.metricColumnChanged()"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-repeat="selectParts in ctrl.selectParts">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">
<span ng-show="$index === 0">SELECT</span>&nbsp;
</label>
</div>
<div class="gf-form" ng-repeat="part in selectParts">
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleSelectPartEvent(selectParts, part, $event)">
</sql-part-editor>
</div>
<div class="gf-form">
<label class="dropdown"
dropdown-typeahead2="ctrl.selectMenu"
dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)"
button-template-class="gf-form-label query-part"
>
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">WHERE</label>
</div>
<div class="gf-form" ng-repeat="part in ctrl.whereParts">
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleWherePartEvent(ctrl.whereParts, part, $event, $index)">
</sql-part-editor>
</div>
<div class="gf-form">
<metric-segment segment="ctrl.whereAdd" get-options="ctrl.getWhereOptions()" on-change="ctrl.addWhereAction(part, $index)"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-6">
<span>GROUP BY</span>
</label>
<sql-part-editor ng-repeat="part in ctrl.groupParts"
part="part" class="gf-form-label sql-part"
handle-event="ctrl.handleGroupPartEvent(part, $index, $event)">
</sql-part-editor>
</div>
<div class="gf-form">
<metric-segment segment="ctrl.groupAdd" get-options="ctrl.getGroupOptions()" on-change="ctrl.addGroupAction(part, $index)"></metric-segment>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword" for="format-select-{{ ctrl.target.refId }}">Format As</label>
<div class="gf-form-select-wrapper">
<select id="format-select-{{ ctrl.target.refId }}" class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleEditorMode()" ng-show="ctrl.panelCtrl.panel.type !== 'table'">
<span ng-show="ctrl.target.rawQuery">Query Builder</span>
<span ng-hide="ctrl.target.rawQuery">Edit SQL</span>
</label>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showHelp = !ctrl.showHelp">
Show Help
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
Generated SQL
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
- return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
- return column(s) with numeric datatype as values
Optional:
- return column named <i>metric</i> to represent the series name.
- If multiple value columns are returned the metric column is used as prefix.
- If no column named metric is found the column name of the value column is used as series name
Resultsets of time series queries need to be sorted by time.
Table:
- return any set of columns
Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeEpoch(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; column BETWEEN FROM_UNIXTIME(1492750877) AND FROM_UNIXTIME(1492750877)
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
- $__unixEpochNanoFilter(column) -&gt; column &gt;= 1494410783152415214 AND column &lt;= 1494497183142514872
- $__timeGroup(column,'5m'[, fillvalue]) -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
by setting fillvalue grafana will fill in missing values according to the interval
fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
- $__timeGroupAlias(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
- $__unixEpochGroup(column,'5m') -&gt; column DIV 300 * 300
- $__unixEpochGroupAlias(column,'5m') -&gt; column DIV 300 * 300 AS "time"
Example of group by and order by with $__timeGroup:
SELECT
$__timeGroupAlias(timestamp_col, '1h'),
sum(value_double) as value
FROM yourtable
GROUP BY 1
ORDER BY 1
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)
- $__timeTo() -&gt; FROM_UNIXTIME(1492750877)
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
- $__unixEpochNanoFrom() -&gt; 1494410783152415214
- $__unixEpochNanoTo() -&gt; 1494497183142514872
</pre>
</div>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.executedQueryString}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
</query-editor-row>

@ -1,647 +0,0 @@
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 '../../../types/events';
import { MysqlMetaQuery } from './meta_query';
import MySQLQueryModel from './mysql_query_model';
import sqlPart from './sql_part';
const defaultQuery = `SELECT
UNIX_TIMESTAMP(<time_column>) as time_sec,
<value column> as value,
<series name column> as metric
FROM <table name>
WHERE $__timeFilter(time_column)
ORDER BY <time_column> ASC
`;
export class MysqlQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
formats: any[];
lastQueryError?: string;
showHelp!: boolean;
queryModel: MySQLQueryModel;
metaBuilder: MysqlMetaQuery;
lastQueryMeta?: QueryResultMeta;
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 MySQLQueryModel(this.target, templateSrv, this.panel.scopedVars);
this.metaBuilder = new MysqlMetaQuery(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();
}
updateProjection() {
this.selectParts = map(this.target.select, (parts: any) => {
return map(parts, sqlPart.create).filter((n) => n);
});
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.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() {
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' },
],
};
this.selectMenu.push(aggregates);
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 }) {
const plusButton = this.uiSegmentSrv.newPlusButton();
button.html = plusButton.html;
button.value = plusButton.value;
}
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: 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 (['int', 'bigint', 'double', 'datetime'].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: number) {
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() {
switch (this.groupAdd.value) {
default: {
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 [];
}
}

@ -1,77 +0,0 @@
import { uniqBy } from 'lodash';
import { AnnotationEvent, DataFrame, MetricFindValue } from '@grafana/data';
import { BackendDataSourceResponse, FetchResponse, toDataQueryResponse } from '@grafana/runtime';
export default class ResponseParser {
transformMetricFindResponse(raw: FetchResponse<BackendDataSourceResponse>): MetricFindValue[] {
const frames = toDataQueryResponse(raw).data as DataFrame[];
if (!frames || !frames.length) {
return [];
}
const frame = frames[0];
const values: MetricFindValue[] = [];
const textField = frame.fields.find((f) => f.name === '__text');
const valueField = frame.fields.find((f) => f.name === '__value');
if (textField && valueField) {
for (let i = 0; i < textField.values.length; i++) {
values.push({ text: '' + textField.values.get(i), value: '' + valueField.values.get(i) });
}
} else {
values.push(
...frame.fields
.flatMap((f) => f.values.toArray())
.map((v) => ({
text: v,
}))
);
}
return uniqBy(values, 'text');
}
async transformAnnotationResponse(options: any, data: BackendDataSourceResponse): Promise<AnnotationEvent[]> {
const frames = toDataQueryResponse({ data: data }).data as DataFrame[];
if (!frames || !frames.length) {
return [];
}
const frame = frames[0];
const timeField = frame.fields.find((f) => f.name === 'time' || f.name === 'time_sec');
if (!timeField) {
throw new Error('Missing mandatory time column (with time column alias) in annotation query');
}
if (frame.fields.find((f) => f.name === 'title')) {
throw new Error('The title column for annotations is deprecated, now only a column named text is returned');
}
const timeEndField = frame.fields.find((f) => f.name === 'timeend');
const textField = frame.fields.find((f) => f.name === 'text');
const tagsField = frame.fields.find((f) => f.name === 'tags');
const list: AnnotationEvent[] = [];
for (let i = 0; i < frame.length; i++) {
const timeEnd = timeEndField && timeEndField.values.get(i) ? Math.floor(timeEndField.values.get(i)) : undefined;
list.push({
annotation: options.annotation,
time: Math.floor(timeField.values.get(i)),
timeEnd,
text: textField && textField.values.get(i) ? textField.values.get(i) : '',
tags:
tagsField && tagsField.values.get(i)
? tagsField.values
.get(i)
.trim()
.split(/\s*,\s*/)
: [],
});
}
return list;
}
}

@ -9,12 +9,12 @@ import {
} from '@grafana/data';
import { FetchResponse, setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { SQLQuery } from 'app/features/plugins/sql/types';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer';
import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer';
import { MysqlDatasource } from '../datasource';
import { MySQLOptions, MySQLQuery } from './../types';
import { MySqlDatasource } from '../MySqlDatasource';
import { MySQLOptions } from '../types';
describe('MySQLDatasource', () => {
const setupTextContext = (response: any) => {
@ -30,7 +30,8 @@ describe('MySQLDatasource', () => {
const variable = { ...initialCustomVariableModelState };
fetchMock.mockImplementation((options) => of(createFetchResponse(response)));
const ds = new MysqlDatasource(instanceSettings, templateSrv);
const ds = new MySqlDatasource(instanceSettings);
Reflect.set(ds, 'templateSrv', templateSrv);
return { ds, variable, templateSrv, fetchMock };
};
@ -52,7 +53,7 @@ describe('MySQLDatasource', () => {
hide: true,
},
],
} as unknown as DataQueryRequest<MySQLQuery>;
} as unknown as DataQueryRequest<SQLQuery>;
const { ds, fetchMock } = setupTextContext({});
@ -63,54 +64,6 @@ describe('MySQLDatasource', () => {
});
});
describe('When performing annotationQuery', () => {
let results: any;
const annotationName = 'MyAnno';
const options = {
annotation: {
name: annotationName,
rawQuery: 'select time_sec, text, tags from table;',
},
range: {
from: dateTime(1432288354),
to: dateTime(1432288401),
},
};
const response = {
results: {
MyAnno: {
frames: [
dataFrameToJSON(
new MutableDataFrame({
fields: [
{ name: 'time_sec', values: [1432288355, 1432288390, 1432288400] },
{ name: 'text', values: ['some text', 'some text2', 'some text3'] },
{ name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] },
],
})
),
],
},
},
};
beforeEach(async () => {
const { ds } = setupTextContext(response);
const data = await ds.annotationQuery(options);
results = data;
});
it('should return annotation list', async () => {
expect(results.length).toBe(3);
expect(results[0].text).toBe('some text');
expect(results[0].tags[0]).toBe('TagA');
expect(results[0].tags[1]).toBe('TagB');
expect(results[1].tags[0]).toBe('TagB');
expect(results[1].tags[1]).toBe('TagC');
expect(results[2].tags.length).toBe(0);
});
});
describe('When performing metricFindQuery that returns multiple string fields', () => {
const query = 'select * from atable';
const response = {
@ -376,6 +329,7 @@ describe('MySQLDatasource', () => {
grafana_metric
WHERE
$__timeFilter(createdAt) AND
foo = 'bar' AND
measurement = 'logins.count' AND
hostname IN($host)
GROUP BY 1, 3
@ -383,6 +337,7 @@ describe('MySQLDatasource', () => {
const query = {
rawSql,
rawQuery: true,
refId: '',
};
templateSrv.init([
{ type: 'query', name: 'summarize', current: { value: '1m' } },
@ -407,6 +362,7 @@ describe('MySQLDatasource', () => {
const query = {
rawSql,
rawQuery: true,
refId: '',
};
templateSrv.init([
{ type: 'query', name: 'summarize', current: { value: '1m' } },

@ -0,0 +1,281 @@
import {
ColumnDefinition,
CompletionItemKind,
CompletionItemPriority,
LanguageCompletionProvider,
LinkedToken,
StatementPlacementProvider,
StatementPosition,
SuggestionKindProvider,
TableDefinition,
TokenType,
} from '@grafana/experimental';
import { PositionContext } from '@grafana/experimental/dist/sql-editor/types';
import { AGGREGATE_FNS, OPERATORS } from 'app/features/plugins/sql/constants';
import { Aggregate, DB, MetaDefinition, SQLQuery } from 'app/features/plugins/sql/types';
import { FUNCTIONS } from './functions';
interface CompletionProviderGetterArgs {
getColumns: React.MutableRefObject<(t: SQLQuery) => Promise<ColumnDefinition[]>>;
getTables: React.MutableRefObject<(d?: string) => Promise<TableDefinition[]>>;
fetchMeta: React.MutableRefObject<(d?: string) => Promise<MetaDefinition[]>>;
getFunctions: React.MutableRefObject<(d?: string) => Aggregate[]>;
}
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider =
({ getColumns, getTables, fetchMeta, getFunctions }) =>
() => ({
triggerCharacters: ['.', ' ', '$', ',', '(', "'"],
supportedFunctions: () => getFunctions.current(),
supportedOperators: () => OPERATORS,
customSuggestionKinds: customSuggestionKinds(getTables, getColumns, fetchMeta),
customStatementPlacement,
});
export enum CustomStatementPlacement {
AfterDataset = 'afterDataset',
AfterFrom = 'afterFrom',
AfterSelect = 'afterSelect',
}
export enum CustomSuggestionKind {
TablesWithinDataset = 'tablesWithinDataset',
}
export enum Direction {
Next = 'next',
Previous = 'previous',
}
const TRIGGER_SUGGEST = 'editor.action.triggerSuggest';
enum Keyword {
Select = 'SELECT',
Where = 'WHERE',
From = 'FROM',
}
export const customStatementPlacement: StatementPlacementProvider = () => [
{
id: CustomStatementPlacement.AfterDataset,
resolve: (currentToken, previousKeyword) => {
return Boolean(
currentToken?.is(TokenType.Delimiter, '.') ||
(currentToken?.is(TokenType.Whitespace) && currentToken?.previous?.is(TokenType.Delimiter, '.'))
);
},
},
{
id: CustomStatementPlacement.AfterFrom,
resolve: (currentToken, previousKeyword) => {
return Boolean(isAfterFrom(currentToken));
},
},
{
id: CustomStatementPlacement.AfterSelect,
resolve: (token, previousKeyword) => {
const is =
isDirectlyAfter(token, Keyword.Select) ||
(isAfterSelect(token) && token?.previous?.is(TokenType.Delimiter, ','));
return Boolean(is);
},
},
];
export const customSuggestionKinds: (
getTables: CompletionProviderGetterArgs['getTables'],
getFields: CompletionProviderGetterArgs['getColumns'],
fetchMeta: CompletionProviderGetterArgs['fetchMeta']
) => SuggestionKindProvider = (getTables, _, fetchMeta) => () =>
[
{
id: CustomSuggestionKind.TablesWithinDataset,
applyTo: [CustomStatementPlacement.AfterDataset],
suggestionsResolver: async (ctx) => {
const tablePath = ctx.currentToken ? getTablePath(ctx.currentToken) : '';
const t = await getTables.current(tablePath);
return t.map((table) => suggestion(table.name, table.completion ?? table.name, CompletionItemKind.Field, ctx));
},
},
{
id: 'metaAfterSelect',
applyTo: [CustomStatementPlacement.AfterSelect],
suggestionsResolver: async (ctx) => {
const path = getPath(ctx.currentToken, Direction.Next);
const t = await fetchMeta.current(path);
return t.map((meta) => {
const completion = meta.kind === CompletionItemKind.Class ? `${meta.completion}.` : meta.completion;
return suggestion(meta.name, completion!, meta.kind, ctx);
});
},
},
{
id: 'metaAfterSelectFuncArg',
applyTo: [StatementPosition.AfterSelectFuncFirstArgument],
suggestionsResolver: async (ctx) => {
const path = getPath(ctx.currentToken, Direction.Next);
const t = await fetchMeta.current(path);
return t.map((meta) => {
const completion = meta.kind === CompletionItemKind.Class ? `${meta.completion}.` : meta.completion;
return suggestion(meta.name, completion!, meta.kind, ctx);
});
},
},
{
id: 'metaAfterFrom',
applyTo: [CustomStatementPlacement.AfterFrom],
suggestionsResolver: async (ctx) => {
// TODO: why is this triggering when isAfterFrom is false
if (!isAfterFrom(ctx.currentToken)) {
return [];
}
const path = ctx.currentToken?.value || '';
const t = await fetchMeta.current(path);
return t.map((meta) => suggestion(meta.name, meta.completion!, meta.kind, ctx));
},
},
{
id: `MYSQL${StatementPosition.WhereKeyword}`,
applyTo: [StatementPosition.WhereKeyword],
suggestionsResolver: async (ctx) => {
const path = getPath(ctx.currentToken, Direction.Previous);
const t = await fetchMeta.current(path);
return t.map((meta) => {
const completion = meta.kind === CompletionItemKind.Class ? `${meta.completion}.` : meta.completion;
return suggestion(meta.name, completion!, meta.kind, ctx);
});
},
},
{
id: StatementPosition.WhereComparisonOperator,
applyTo: [StatementPosition.WhereComparisonOperator],
suggestionsResolver: async (ctx) => {
if (!isAfterWhere(ctx.currentToken)) {
return [];
}
const path = getPath(ctx.currentToken, Direction.Previous);
const t = await fetchMeta.current(path);
const sugg = t.map((meta) => {
const completion = meta.kind === CompletionItemKind.Class ? `${meta.completion}.` : meta.completion;
return suggestion(meta.name, completion!, meta.kind, ctx);
});
return sugg;
},
},
];
function getPath(token: LinkedToken | null, direction: Direction) {
let path = token?.value || '';
const fromValue = keywordValue(token, Keyword.From, direction);
if (fromValue) {
path = fromValue;
}
return path;
}
export function getTablePath(token: LinkedToken) {
let processedToken = token;
let tablePath = '';
while (processedToken?.previous && !processedToken.previous.isWhiteSpace()) {
processedToken = processedToken.previous;
tablePath = processedToken.value + tablePath;
}
tablePath = tablePath.trim();
return tablePath;
}
function suggestion(label: string, completion: string, kind: CompletionItemKind, ctx: PositionContext) {
return {
label,
insertText: completion,
command: { id: TRIGGER_SUGGEST, title: '' },
kind,
sortText: CompletionItemPriority.High,
range: {
...ctx.range,
startColumn: ctx.range.endColumn,
endColumn: ctx.range.endColumn,
},
};
}
function isAfterSelect(token: LinkedToken | null) {
return isAfterKeyword(token, Keyword.Select);
}
function isAfterFrom(token: LinkedToken | null) {
return isDirectlyAfter(token, Keyword.From);
}
function isAfterWhere(token: LinkedToken | null) {
return isAfterKeyword(token, Keyword.Where);
}
function isAfterKeyword(token: LinkedToken | null, keyword: string) {
if (!token?.is(TokenType.Keyword)) {
let curToken = token;
while (true) {
if (!curToken) {
return false;
}
if (curToken.is(TokenType.Keyword, keyword)) {
return true;
}
if (curToken.isKeyword()) {
return false;
}
curToken = curToken?.previous || null;
}
}
return false;
}
function isDirectlyAfter(token: LinkedToken | null, keyword: string) {
return token?.is(TokenType.Whitespace) && token?.previous?.is(TokenType.Keyword, keyword);
}
function keywordValue(token: LinkedToken | null, keyword: Keyword, direction: Direction) {
let next = token;
while (next) {
if (next.is(TokenType.Keyword, keyword)) {
return tokenValue(next);
}
next = next[direction];
}
return false;
}
function tokenValue(token: LinkedToken | null): string | undefined {
const ws = token?.next;
if (ws?.isWhiteSpace()) {
const v = ws.next;
const delim = v?.next;
if (!delim?.is(TokenType.Delimiter)) {
return v?.value;
}
return `${v?.value}${delim?.value}${delim.next?.value}`;
}
return undefined;
}
export async function fetchColumns(db: DB, q: SQLQuery) {
const cols = await db.fields(q);
if (cols.length > 0) {
return cols.map((c) => {
return { name: c.value, type: c.value, description: c.value };
});
} else {
return [];
}
}
export async function fetchTables(db: DB, q: Partial<SQLQuery>) {
const tables = await db.lookup(q.dataset);
return tables;
}
export function getFunctions(): Aggregate[] {
return [...AGGREGATE_FNS, ...FUNCTIONS];
}

@ -1,86 +0,0 @@
import { SqlPartDef, SqlPart } from 'app/angular/components/sql_part/sql_part';
const index: any[] = [];
function createPart(part: any): any {
const def = index[part.type];
if (!def) {
return null;
}
return new SqlPart(part, def);
}
function register(options: any) {
index[options.type] = new SqlPartDef(options);
}
register({
type: 'column',
style: 'label',
params: [{ type: 'column', dynamicLookup: true }],
defaultParams: ['value'],
});
register({
type: 'expression',
style: 'expression',
label: 'Expr:',
params: [
{ name: 'left', type: 'string', dynamicLookup: true },
{ name: 'op', type: 'string', dynamicLookup: true },
{ name: 'right', type: 'string', dynamicLookup: true },
],
defaultParams: ['value', '=', 'value'],
});
register({
type: 'macro',
style: 'label',
label: 'Macro:',
params: [],
defaultParams: [],
});
register({
type: 'aggregate',
style: 'label',
params: [
{
name: 'name',
type: 'string',
options: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'],
},
],
defaultParams: ['avg'],
});
register({
type: 'alias',
style: 'label',
params: [{ name: 'name', type: 'string', quote: 'double' }],
defaultParams: ['alias'],
});
register({
type: 'time',
style: 'function',
label: 'time',
params: [
{
name: 'interval',
type: 'interval',
options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'],
},
{
name: 'fill',
type: 'string',
options: ['none', 'NULL', 'previous', '0'],
},
],
defaultParams: ['$__interval', 'none'],
});
export default {
create: createPart,
};

@ -1,5 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/types';
import { SQLOptions, SQLQuery } from 'app/features/plugins/sql/types';
export interface MysqlQueryForInterpolation {
alias?: any;
format?: any;
@ -8,21 +8,6 @@ export interface MysqlQueryForInterpolation {
hide?: any;
}
export interface MySQLOptions extends DataSourceJsonData, SQLConnectionLimits {
tlsAuth: boolean;
tlsAuthWithCACert: boolean;
timezone: string;
tlsSkipVerify: boolean;
user: string;
database: string;
url: string;
timeInterval: string;
}
export type ResultFormat = 'time_series' | 'table';
export interface MySQLOptions extends SQLOptions {}
export interface MySQLQuery extends DataQuery {
alias?: string;
format?: ResultFormat;
rawSql?: any;
}
export interface MySQLQuery extends SQLQuery {}

Loading…
Cancel
Save