diff --git a/.betterer.results b/.betterer.results index 715969079db..343a79f5634 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index fe561d3188d..0695797b999 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -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 diff --git a/public/app/features/plugins/sql/components/visual-query-builder/SQLOrderByRow.tsx b/public/app/features/plugins/sql/components/visual-query-builder/SQLOrderByRow.tsx index 80a5d50b213..8b8631290bc 100644 --- a/public/app/features/plugins/sql/components/visual-query-builder/SQLOrderByRow.tsx +++ b/public/app/features/plugins/sql/components/visual-query-builder/SQLOrderByRow.tsx @@ -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, diff --git a/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx b/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx index 29844d0e4ce..ca3470d0ce0 100644 --- a/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx +++ b/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx @@ -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 ; + const state = useAsync(async () => { + const functions = await db.functions(); + return functions.map((f) => toOption(f.name)); + }, [db]); + + return ; } diff --git a/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx b/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx index f340989d5ae..b11c7e5b357 100644 --- a/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx +++ b/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx @@ -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>; + functions?: Array>; } 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)} /> @@ -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 | null { const column = parameters?.find((p) => p.type === QueryEditorExpressionType.FunctionParameter); if (column?.name) { diff --git a/public/app/features/plugins/sql/datasource/SqlDatasource.ts b/public/app/features/plugins/sql/datasource/SqlDatasource.ts index 6737b6ca645..3adf02b4944 100644 --- a/public/app/features/plugins/sql/datasource/SqlDatasource.ts +++ b/public/app/features/plugins/sql/datasource/SqlDatasource.ts @@ -204,6 +204,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend Promise; datasets: () => Promise; @@ -123,6 +139,7 @@ export interface DB { lookup: (path?: string) => Promise>; getSqlCompletionProvider: () => LanguageCompletionProvider; toRawSql?: (query: SQLQuery) => string; + functions: () => Promise; } export interface QueryEditorProps { diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index b3766a58d98..2447d8acaa2 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -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, }; } } diff --git a/public/app/plugins/datasource/mysql/MySqlDatasource.ts b/public/app/plugins/datasource/mysql/MySqlDatasource.ts new file mode 100644 index 00000000000..49dce6901aa --- /dev/null +++ b/public/app/plugins/datasource/mysql/MySqlDatasource.ts @@ -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) { + super(instanceSettings); + this.responseParser = new MySqlResponseParser(); + this.completionProvider = undefined; + } + + getQueryModel(target?: Partial, 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 { + const datasets = await this.runSql(showDatabases(), { refId: 'datasets' }); + return datasets.map((t) => t[0]); + } + + async fetchTables(dataset?: string): Promise { + const tables = await this.runSql(buildTableQuery(dataset), { refId: 'tables' }); + return tables.map((t) => t[0]); + } + + async fetchFields(query: Partial) { + if (!query.dataset || !query.table) { + return []; + } + const queryString = buildColumnQuery(this.getQueryModel(query), query.table!); + const frame = await this.runSql(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(), + }; + } +} diff --git a/public/app/plugins/datasource/mysql/MySqlQueryModel.ts b/public/app/plugins/datasource/mysql/MySqlQueryModel.ts new file mode 100644 index 00000000000..dbd0fee6895 --- /dev/null +++ b/public/app/plugins/datasource/mysql/MySqlQueryModel.ts @@ -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; + templateSrv?: TemplateSrv; + scopedVars?: ScopedVars; + + constructor(target: Partial, 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; + } +} diff --git a/public/app/plugins/datasource/mysql/MySqlResponseParser.ts b/public/app/plugins/datasource/mysql/MySqlResponseParser.ts new file mode 100644 index 00000000000..d4ef6c9f0d7 --- /dev/null +++ b/public/app/plugins/datasource/mysql/MySqlResponseParser.ts @@ -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'); + } +} diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts deleted file mode 100644 index 904099254e0..00000000000 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ /dev/null @@ -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 { - id: any; - name: any; - responseParser: ResponseParser; - queryModel: MySQLQueryModel; - interval: string; - - constructor( - instanceSettings: DataSourceInstanceSettings, - 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 { - 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 { - 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({ - 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) => - await this.responseParser.transformAnnotationResponse(options, res.data) - ) - ) - ); - } - - metricFindQuery(query: string, optionalOptions: any): Promise { - 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({ - 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 { - 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); - } -} diff --git a/public/app/plugins/datasource/mysql/fields.ts b/public/app/plugins/datasource/mysql/fields.ts new file mode 100644 index 00000000000..96eb122c35e --- /dev/null +++ b/public/app/plugins/datasource/mysql/fields.ts @@ -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; + } +} diff --git a/public/app/plugins/datasource/mysql/functions.ts b/public/app/plugins/datasource/mysql/functions.ts new file mode 100644 index 00000000000..0ccb8ce0165 --- /dev/null +++ b/public/app/plugins/datasource/mysql/functions.ts @@ -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.`, + }, +]; diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts deleted file mode 100644 index eb03dc40f99..00000000000 --- a/public/app/plugins/datasource/mysql/meta_query.ts +++ /dev/null @@ -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; - } -} diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index 7044b6dd35b..c53f5b5594c 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -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() as time_sec, - as text, - as tags - FROM - WHERE $__timeFilter(time_column) - ORDER BY 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) - .setQueryCtrl(MysqlQueryCtrl) - .setConfigEditor(ConfigurationEditor) - .setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl); +export const plugin = new DataSourcePlugin(MySqlDatasource) + .setQueryEditor(SqlQueryEditor) + .setConfigEditor(ConfigurationEditor); diff --git a/public/app/plugins/datasource/mysql/mySqlMetaQuery.ts b/public/app/plugins/datasource/mysql/mySqlMetaQuery.ts new file mode 100644 index 00000000000..25d09bcb8e0 --- /dev/null +++ b/public/app/plugins/datasource/mysql/mySqlMetaQuery.ts @@ -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)); +} diff --git a/public/app/plugins/datasource/mysql/mysql_query_model.ts b/public/app/plugins/datasource/mysql/mysql_query_model.ts deleted file mode 100644 index 4fc106f9072..00000000000 --- a/public/app/plugins/datasource/mysql/mysql_query_model.ts +++ /dev/null @@ -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; - } -} diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html deleted file mode 100644 index a9530b03f8e..00000000000 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ /dev/null @@ -1,54 +0,0 @@ -
-
-
- -
-
- -
-
- -
-
- -
-
Annotation Query Format
-An annotation is an event that is overlaid on top of graphs. The query can have up to four columns per row, the time or time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. - -- column with alias: time or time_sec for the annotation event time. Use epoch time or any native date data type. -- column with alias: timeend for the annotation event end time. Use epoch time or any native date data type. -- column with alias: text for the annotation text -- column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' - - -Macros: -- $__time(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) -- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) -- $__timeFilter(column) -> column BETWEEN FROM_UNIXTIME(1492750877) AND FROM_UNIXTIME(1492750877) -- $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 -- $__unixEpochNanoFilter(column) -> column >= 1494410783152415214 AND column <= 1494497183142514872 - -Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> FROM_UNIXTIME(1492750877) -- $__timeTo() -> FROM_UNIXTIME(1492750877) -- $__unixEpochFrom() -> 1492750877 -- $__unixEpochTo() -> 1492750877 -- $__unixEpochNanoFrom() -> 1494410783152415214 -- $__unixEpochNanoTo() -> 1494497183142514872 -
-
-
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html deleted file mode 100644 index 014135413b6..00000000000 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ /dev/null @@ -1,190 +0,0 @@ - - -
-
-
- - -
-
-
- -
-
-
- - - - - - - - -
- -
-
-
- -
- -
-
- -
- -
- - -
- -
- -
- -
-
-
-
- -
-
- -
- -
- - -
- -
- -
- -
-
-
- -
- -
-
- - - - -
- -
- -
- -
-
-
-
- -
- -
-
- -
- -
-
-
- -
-
- -
-
- -
-
-
-
-
- -
-
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 metric 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) -> UNIX_TIMESTAMP(column) as time_sec
-- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time_sec
-- $__timeFilter(column) -> column BETWEEN FROM_UNIXTIME(1492750877) AND FROM_UNIXTIME(1492750877)
-- $__unixEpochFilter(column) ->  time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877
-- $__unixEpochNanoFilter(column) ->  column >= 1494410783152415214 AND column <= 1494497183142514872
-- $__timeGroup(column,'5m'[, fillvalue]) -> 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') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
-- $__unixEpochGroup(column,'5m') -> column DIV 300 * 300
-- $__unixEpochGroupAlias(column,'5m') -> 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() -> FROM_UNIXTIME(1492750877)
-- $__timeTo() ->  FROM_UNIXTIME(1492750877)
-- $__unixEpochFrom() ->  1492750877
-- $__unixEpochTo() ->  1492750877
-- $__unixEpochNanoFrom() ->  1494410783152415214
-- $__unixEpochNanoTo() ->  1494497183142514872
-    
-
- - - -
-
{{ctrl.lastQueryMeta.executedQueryString}}
-
- -
-
{{ctrl.lastQueryError}}
-
- -
diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts deleted file mode 100644 index 62a9d69cf93..00000000000 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ /dev/null @@ -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() as time_sec, - as value, - as metric -FROM
-WHERE $__timeFilter(time_column) -ORDER BY 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 []; - } -} diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts deleted file mode 100644 index ff16bd60551..00000000000 --- a/public/app/plugins/datasource/mysql/response_parser.ts +++ /dev/null @@ -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): 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 { - 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; - } -} diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 54ab5575079..6e977681d00 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -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; + } as unknown as DataQueryRequest; 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' } }, diff --git a/public/app/plugins/datasource/mysql/sqlCompletionProvider.ts b/public/app/plugins/datasource/mysql/sqlCompletionProvider.ts new file mode 100644 index 00000000000..59adea66426 --- /dev/null +++ b/public/app/plugins/datasource/mysql/sqlCompletionProvider.ts @@ -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>; + getTables: React.MutableRefObject<(d?: string) => Promise>; + fetchMeta: React.MutableRefObject<(d?: string) => Promise>; + 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) { + const tables = await db.lookup(q.dataset); + return tables; +} + +export function getFunctions(): Aggregate[] { + return [...AGGREGATE_FNS, ...FUNCTIONS]; +} diff --git a/public/app/plugins/datasource/mysql/sql_part.ts b/public/app/plugins/datasource/mysql/sql_part.ts deleted file mode 100644 index 7506431c3d0..00000000000 --- a/public/app/plugins/datasource/mysql/sql_part.ts +++ /dev/null @@ -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, -}; diff --git a/public/app/plugins/datasource/mysql/types.ts b/public/app/plugins/datasource/mysql/types.ts index aadcaa1f84f..dc1702c8b51 100644 --- a/public/app/plugins/datasource/mysql/types.ts +++ b/public/app/plugins/datasource/mysql/types.ts @@ -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 interface MySQLOptions extends SQLOptions {} -export type ResultFormat = 'time_series' | 'table'; - -export interface MySQLQuery extends DataQuery { - alias?: string; - format?: ResultFormat; - rawSql?: any; -} +export interface MySQLQuery extends SQLQuery {}