diff --git a/.betterer.results b/.betterer.results index c5d0b69eb13..715969079db 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7778,52 +7778,6 @@ 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/mssql/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, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"] - ], - "public/app/plugins/datasource/mssql/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/mssql/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"] - ], - "public/app/plugins/datasource/mssql/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/mssql/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"], - [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"] - ], - "public/app/plugins/datasource/mssql/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"] - ], "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"], diff --git a/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx b/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx index 5b9dad73c73..602365c9954 100644 --- a/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx +++ b/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { FieldSet, InlineField } from '@grafana/ui'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; -import { SQLConnectionLimits } from './types'; +import { SQLConnectionLimits } from '../../types'; interface Props { onPropertyChanged: (property: keyof T, value?: number) => void; diff --git a/public/app/features/plugins/sql/components/configuration/types.ts b/public/app/features/plugins/sql/components/configuration/types.ts deleted file mode 100644 index 00678993762..00000000000 --- a/public/app/features/plugins/sql/components/configuration/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface SQLConnectionLimits { - maxOpenConns: number; - maxIdleConns: number; - connMaxLifetime: number; -} diff --git a/public/app/features/plugins/sql/datasource/SqlDatasource.ts b/public/app/features/plugins/sql/datasource/SqlDatasource.ts index 8039583dad2..6737b6ca645 100644 --- a/public/app/features/plugins/sql/datasource/SqlDatasource.ts +++ b/public/app/features/plugins/sql/datasource/SqlDatasource.ts @@ -25,15 +25,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { VariableWithMultiSupport } from '../../../variables/types'; import { getSearchFilterScopedVar, SearchFilterOptions } from '../../../variables/utils'; import { MACRO_NAMES } from '../constants'; -import { - DB, - SQLQuery, - SQLOptions, - SqlQueryForInterpolation, - ResponseParser, - SqlQueryModel, - QueryFormat, -} from '../types'; +import { DB, SQLQuery, SQLOptions, ResponseParser, SqlQueryModel, QueryFormat } from '../types'; export abstract class SqlDatasource extends DataSourceWithBackend { id: number; @@ -82,10 +74,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend 0) { expandedQueries = queries.map((query) => { @@ -141,8 +130,8 @@ export abstract class SqlDatasource extends DataSourceWithBackend(query: string, options?: MetricFindQueryOptions) { - const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table }, options); + async runSql(query: string, options?: RunSQLOptions) { + const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table, refId: options?.refId }, options); return new DataFrameView(frame); } @@ -212,6 +201,9 @@ export abstract class SqlDatasource extends DataSourceWithBackend ({ - ...(jest.requireActual('@grafana/runtime') as unknown as object), + ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, })); +const instanceSettings = { + id: 1, + uid: 'mssql-datasource', + type: 'mssql', + name: 'MSSQL', + access: 'direct', +} as DataSourceInstanceSettings; + describe('MSSQLDatasource', () => { - const templateSrv: TemplateSrv = new TemplateSrv(); const fetchMock = jest.spyOn(backendSrv, 'fetch'); - const ctx: any = {}; + const ctx = { + ds: new MssqlDatasource(instanceSettings), + variable: { ...initialCustomVariableModelState }, + }; beforeEach(() => { jest.clearAllMocks(); - ctx.instanceSettings = { name: 'mssql' }; - ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv); - }); - - describe('When performing annotationQuery', () => { - let results: any; - - const annotationName = 'MyAnno'; - - const options = { - annotation: { - name: annotationName, - rawQuery: 'select time, text, tags from table;', - }, - range: { - from: dateTime(1432288354), - to: dateTime(1432288401), - }, - }; - - const response = { - results: { - MyAnno: { - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'time', values: [1521545610656, 1521546251185, 1521546501378] }, - { name: 'text', values: ['some text', 'some text2', 'some text3'] }, - { name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] }, - ], - }) - ), - ], - }, - }, - }; - - beforeEach(() => { - fetchMock.mockImplementation(() => of(createFetchResponse(response))); - - return ctx.ds.annotationQuery(options).then((data: any) => { - results = data; - }); - }); - - it('should return annotation list', () => { - 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); - }); + ctx.ds = new MssqlDatasource(instanceSettings); }); describe('When performing metricFindQuery that returns multiple string fields', () => { @@ -118,7 +81,7 @@ describe('MSSQLDatasource', () => { }); describe('When performing metricFindQuery with key, value columns', () => { - let results: any; + let results: MetricFindValue[]; const query = 'select * from atable'; const response = { results: { @@ -140,7 +103,7 @@ describe('MSSQLDatasource', () => { beforeEach(() => { fetchMock.mockImplementation(() => of(createFetchResponse(response))); - return ctx.ds.metricFindQuery(query).then((data: any) => { + return ctx.ds.metricFindQuery(query).then((data) => { results = data; }); }); @@ -155,7 +118,7 @@ describe('MSSQLDatasource', () => { }); describe('When performing metricFindQuery without key, value columns', () => { - let results: any; + let results: MetricFindValue[]; const query = 'select id, values from atable'; const response = { results: { @@ -181,7 +144,7 @@ describe('MSSQLDatasource', () => { beforeEach(() => { fetchMock.mockImplementation(() => of(createFetchResponse(response))); - return ctx.ds.metricFindQuery(query).then((data: any) => { + return ctx.ds.metricFindQuery(query).then((data) => { results = data; }); }); @@ -199,7 +162,7 @@ describe('MSSQLDatasource', () => { }); describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => { - let results: any; + let results: MetricFindValue[]; const query = 'select * from atable'; const response = { results: { @@ -220,7 +183,7 @@ describe('MSSQLDatasource', () => { beforeEach(() => { fetchMock.mockImplementation(() => of(createFetchResponse(response))); - return ctx.ds.metricFindQuery(query).then((data: any) => { + return ctx.ds.metricFindQuery(query).then((data) => { results = data; }); }); @@ -247,9 +210,10 @@ describe('MSSQLDatasource', () => { }, }, }; - const time = { + const time: TimeRange = { from: dateTime(1521545610656), to: dateTime(1521546251185), + raw: { from: '1521545610656', to: '1521546251185' }, }; beforeEach(() => { @@ -268,10 +232,6 @@ describe('MSSQLDatasource', () => { }); describe('When interpolating variables', () => { - beforeEach(() => { - ctx.variable = { ...initialCustomVariableModelState }; - }); - describe('and value is a string', () => { it('should return an unquoted value', () => { expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); @@ -314,6 +274,7 @@ describe('MSSQLDatasource', () => { describe('targetContainsTemplate', () => { it('given query that contains template variable it should return true', () => { + const templateSrv = new TemplateSrv(); const rawSql = `SELECT $__timeGroup(createdAt,'$summarize') as time, avg(value) as value, @@ -326,17 +287,21 @@ describe('MSSQLDatasource', () => { hostname IN($host) GROUP BY $__timeGroup(createdAt,'$summarize'), hostname ORDER BY 1`; - const query = { + const query: SQLQuery = { rawSql, + refId: 'A', }; templateSrv.init([ { type: 'query', name: 'summarize', current: { value: '1m' } }, { type: 'query', name: 'host', current: { value: 'a' } }, ]); - expect(ctx.ds.targetContainsTemplate(query)).toBeTruthy(); + const ds = new MssqlDatasource(instanceSettings, templateSrv); + + expect(ds.targetContainsTemplate(query)).toBeTruthy(); }); it('given query that only contains global template variable it should return false', () => { + const templateSrv: TemplateSrv = new TemplateSrv(); const rawSql = `SELECT $__timeGroup(createdAt,'$__interval') as time, avg(value) as value, @@ -348,14 +313,16 @@ describe('MSSQLDatasource', () => { measurement = 'logins.count' GROUP BY $__timeGroup(createdAt,'$summarize'), hostname ORDER BY 1`; - const query = { + const query: SQLQuery = { rawSql, + refId: 'A', }; templateSrv.init([ { type: 'query', name: 'summarize', current: { value: '1m' } }, { type: 'query', name: 'host', current: { value: 'a' } }, ]); - expect(ctx.ds.targetContainsTemplate(query)).toBeFalsy(); + const ds = new MssqlDatasource(instanceSettings, templateSrv); + expect(ds.targetContainsTemplate(query)).toBeFalsy(); }); }); }); diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 879cd972b50..b3766a58d98 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -1,191 +1,97 @@ -import { map as _map } from 'lodash'; -import { lastValueFrom, of } from 'rxjs'; -import { catchError, map, mapTo } from 'rxjs/operators'; +import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { LanguageCompletionProvider } from '@grafana/experimental'; +import { TemplateSrv } from '@grafana/runtime'; +import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource'; +import { DB, ResponseParser, SQLQuery, SQLSelectableValue } from 'app/features/plugins/sql/types'; -import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars, TimeRange } 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 { getSchema, showDatabases, showTables } from './MSSqlMetaQuery'; +import { MSSqlQueryModel } from './MSSqlQueryModel'; +import { MSSqlResponseParser } from './response_parser'; +import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider'; +import { getIcon, getRAQBType, SCHEMA_NAME, toRawSql } from './sqlUtil'; +import { MssqlOptions } from './types'; -import ResponseParser from './response_parser'; -import { MssqlOptions, MssqlQuery, MssqlQueryForInterpolation } from './types'; - -export class MssqlDatasource extends DataSourceWithBackend { - id: any; - name: any; - responseParser: ResponseParser; - interval: string; - - constructor( - instanceSettings: DataSourceInstanceSettings, - private readonly templateSrv: TemplateSrv = getTemplateSrv() - ) { - super(instanceSettings); - this.name = instanceSettings.name; - this.id = instanceSettings.id; - this.responseParser = new ResponseParser(); - const settingsData = instanceSettings.jsonData || ({} as MssqlOptions); - this.interval = settingsData.timeInterval || '1m'; +export class MssqlDatasource extends SqlDatasource { + completionProvider: LanguageCompletionProvider | undefined = undefined; + constructor(instanceSettings: DataSourceInstanceSettings, templateSrv?: TemplateSrv) { + super(instanceSettings, templateSrv); } - interpolateVariable(value: any, variable: any) { - if (typeof value === 'string') { - if (variable.multi || variable.includeAll) { - return "'" + value.replace(/'/g, `''`) + "'"; - } else { - return value; - } - } - - if (typeof value === 'number') { - return value; - } - - const quotedValues = _map(value, (val) => { - if (typeof value === 'number') { - return value; - } - - return "'" + val.replace(/'/g, `''`) + "'"; - }); - return quotedValues.join(','); + getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): MSSqlQueryModel { + return new MSSqlQueryModel(target, templateSrv, scopedVars); } - interpolateVariablesInQueries( - queries: MssqlQueryForInterpolation[], - scopedVars: ScopedVars - ): MssqlQueryForInterpolation[] { - 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; + getResponseParser(): ResponseParser { + return new MSSqlResponseParser(); } - applyTemplateVariables(target: MssqlQuery, scopedVars: ScopedVars): Record { + async fetchDatasets(): Promise { + const datasets = await this.runSql<{ name: string[] }>(showDatabases(), { refId: 'datasets' }); + return datasets.fields.name.values.toArray().flat(); + } + + async fetchTables(dataset?: string): Promise { + const tables = await this.runSql<{ name: string[] }>(showTables(dataset), { refId: 'tables' }); + return tables.fields.name.values.toArray().flat(); + } + + async fetchFields(query: SQLQuery): Promise { + const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.table), { refId: 'columns' }); + const result: SQLSelectableValue[] = []; + for (let i = 0; i < schema.length; i++) { + const column = schema.fields.column.values.get(i); + const type = schema.fields.type.values.get(i); + result.push({ label: column, value: column, type, icon: getIcon(type), raqbFieldType: getRAQBType(type) }); + } + return result; + } + + 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) }, + }; + this.completionProvider = getSqlCompletionProvider(args); + return this.completionProvider; + } + + getDB(): DB { return { - refId: target.refId, - datasource: this.getRef(), - rawSql: this.templateSrv.replace(target.rawSql, scopedVars, this.interpolateVariable), - format: target.format, + init: () => Promise.resolve(true), + datasets: () => this.fetchDatasets(), + tables: (dataset?: string) => this.fetchTables(dataset), + getSqlCompletionProvider: () => this.getSqlCompletionProvider(this.db), + fields: async (query: SQLQuery) => { + if (!query?.dataset && !query?.table) { + return []; + } + return this.fetchFields(query); + }, + validateQuery: (query) => + Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }), + dsID: () => this.id, + dispose: (dsID?: string) => {}, + toRawSql, + lookup: async (path?: string) => { + if (!path) { + const datasets = await this.fetchDatasets(); + return datasets.map((d) => ({ name: d, completion: `${d}.${SCHEMA_NAME}.` })); + } else { + const parts = path.split('.').filter((s: string) => s); + if (parts.length > 2) { + return []; + } + if (parts.length === 1) { + const tables = await this.fetchTables(parts[0]); + return tables.map((t) => ({ name: t, completion: `${t}` })); + } else { + return []; + } + } + }, }; } - - 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) - ) - ) - ); - } - - filterQuery(query: MssqlQuery): boolean { - return !query.hide; - } - - metricFindQuery(query: string, optionalOptions: any): Promise { - let refId = 'tempvar'; - if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) { - refId = optionalOptions.variable.name; - } - - const range = optionalOptions?.range as TimeRange; - - const interpolatedQuery = { - refId: refId, - datasource: this.getRef(), - rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable), - format: 'table', - }; - - 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(query: MssqlQuery): boolean { - const rawSql = query.rawSql.replace('$__', ''); - return this.templateSrv.containsTemplate(rawSql); - } } diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index bc6efb99f7e..1f09748e344 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -1,34 +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 { ConfigurationEditor } from './configuration/ConfigurationEditor'; import { MssqlDatasource } from './datasource'; -import { MssqlQueryCtrl } from './query_ctrl'; -import { MssqlQuery } from './types'; +import { MssqlOptions } from './types'; -const defaultQuery = `SELECT - as time, - as text, - as tags - FROM - - WHERE - $__timeFilter(time_column) - ORDER BY - ASC`; - -class MssqlAnnotationsQueryCtrl { - 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 const plugin = new DataSourcePlugin(MssqlDatasource) - .setQueryCtrl(MssqlQueryCtrl) - .setConfigEditor(ConfigurationEditor) - .setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl); +export const plugin = new DataSourcePlugin(MssqlDatasource) + .setQueryEditor(SqlQueryEditor) + .setConfigEditor(ConfigurationEditor); diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html deleted file mode 100644 index 6919c5ed2c4..00000000000 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ /dev/null @@ -1,90 +0,0 @@ - -
-
- - -
-
- -
-
- -
- -
-
-
- -
-
- -
-
-
-
-
- -
-
Time series:
-- return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
-- any other columns returned will be the time point 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) -> column AS time
-- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
-- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
-- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
-- $__unixEpochNanoFilter(column) ->  column >= 1494410783152415214 AND column <= 1494497183142514872
-- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300.
-     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'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
-- $__unixEpochGroup(column,'5m') -> FLOOR(column/300)*300
-- $__unixEpochGroupAlias(column,'5m') -> FLOOR(column/300)*300 AS [time]
-
-Example of group by and order by with $__timeGroup:
-SELECT
-  $__timeGroup(date_time_col, '1h') AS time,
-  sum(value) as value
-FROM yourtable
-GROUP BY $__timeGroup(date_time_col, '1h')
-ORDER BY 1
-
-Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() ->  '2017-04-21T05:01:17Z'
-- $__timeTo() ->  '2017-04-21T05:01:17Z'
-- $__unixEpochFrom() -> 1492750877
-- $__unixEpochTo() -> 1492750877
-- $__unixEpochNanoFrom() ->  1494410783152415214
-- $__unixEpochNanoTo() ->  1494497183142514872
-		
-
- - - -
-
{{ctrl.lastQueryMeta.executedQueryString}}
-
- -
-
{{ctrl.lastQueryError}}
-
- -
diff --git a/public/app/plugins/datasource/mssql/query_ctrl.ts b/public/app/plugins/datasource/mssql/query_ctrl.ts deleted file mode 100644 index 2876cbd7395..00000000000 --- a/public/app/plugins/datasource/mssql/query_ctrl.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { auto } from 'angular'; - -import { PanelEvents, QueryResultMeta } from '@grafana/data'; -import { QueryCtrl } from 'app/plugins/sdk'; - -import { MssqlQuery } from './types'; - -const defaultQuery = `SELECT - $__timeEpoch(), - as value, - as metric -FROM -
-WHERE - $__timeFilter(time_column) -ORDER BY - ASC`; - -export class MssqlQueryCtrl extends QueryCtrl { - static templateUrl = 'partials/query.editor.html'; - - formats: any[]; - lastQueryMeta?: QueryResultMeta; - lastQueryError?: string; - showHelp = false; - - /** @ngInject */ - constructor($scope: any, $injector: auto.IInjectorService) { - super($scope, $injector); - - this.target.format = this.target.format || 'time_series'; - this.target.alias = ''; - 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'; - } else { - this.target.rawSql = defaultQuery; - } - } - - this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope); - this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope); - } - - 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; - } - } - } -} diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index 52e2af016e3..a23672002e1 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -1,18 +1,10 @@ import { uniqBy } from 'lodash'; -import { AnnotationEvent, DataFrame, MetricFindValue } from '@grafana/data'; -import { BackendDataSourceResponse, toDataQueryResponse, FetchResponse } 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]; +import { DataFrame, MetricFindValue } from '@grafana/data'; +import { ResponseParser } from 'app/features/plugins/sql/types'; +export class MSSqlResponseParser implements 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'); @@ -33,41 +25,4 @@ export default class ResponseParser { 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'); - - if (!timeField) { - return Promise.reject({ message: 'Missing mandatory time column (with time column alias) in annotation query.' }); - } - - 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/mssql/sqlCompletionProvider.ts b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts new file mode 100644 index 00000000000..cc02003ae13 --- /dev/null +++ b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts @@ -0,0 +1,136 @@ +import { + ColumnDefinition, + CompletionItemKind, + CompletionItemPriority, + LanguageCompletionProvider, + LinkedToken, + StatementPlacementProvider, + SuggestionKindProvider, + TableDefinition, + TokenType, +} from '@grafana/experimental'; +import { AGGREGATE_FNS, OPERATORS } from 'app/features/plugins/sql/constants'; +import { DB, SQLQuery } from 'app/features/plugins/sql/types'; + +import { SCHEMA_NAME } from './sqlUtil'; + +interface CompletionProviderGetterArgs { + getColumns: React.MutableRefObject<(t: SQLQuery) => Promise>; + getTables: React.MutableRefObject<(d?: string) => Promise>; +} + +export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider = + ({ getColumns, getTables }) => + () => ({ + triggerCharacters: ['.', ' ', '$', ',', '(', "'"], + tables: { + resolve: async () => { + return await getTables.current(); + }, + parseName: (token: LinkedToken) => { + let processedToken = token; + let tablePath = processedToken.value; + + while (processedToken.next && processedToken.next.type !== TokenType.Whitespace) { + tablePath += processedToken.next.value; + processedToken = processedToken.next; + } + + const tableName = tablePath.split('.').pop(); + + return tableName || tablePath; + }, + }, + + columns: { + resolve: async (t: string) => { + return await getColumns.current({ table: t, refId: 'A' }); + }, + }, + supportedFunctions: () => AGGREGATE_FNS, + supportedOperators: () => OPERATORS, + customSuggestionKinds: customSuggestionKinds(getTables, getColumns), + customStatementPlacement, + }); + +export enum CustomStatementPlacement { + AfterDatabase = 'afterDatabase', +} + +export enum CustomSuggestionKind { + TablesWithinDatabase = 'tablesWithinDatabase', +} + +export const customStatementPlacement: StatementPlacementProvider = () => [ + { + id: CustomStatementPlacement.AfterDatabase, + resolve: (currentToken, previousKeyword) => { + return Boolean( + currentToken?.is(TokenType.Delimiter, '.') || + (currentToken?.is(TokenType.Whitespace) && currentToken?.previous?.is(TokenType.Delimiter, '.')) || + (currentToken?.isNumber() && currentToken.value.endsWith('.')) + ); + }, + }, +]; + +export const customSuggestionKinds: ( + getTables: CompletionProviderGetterArgs['getTables'], + getFields: CompletionProviderGetterArgs['getColumns'] +) => SuggestionKindProvider = (getTables) => () => + [ + { + id: CustomSuggestionKind.TablesWithinDatabase, + applyTo: [CustomStatementPlacement.AfterDatabase], + suggestionsResolver: async (ctx) => { + const tablePath = ctx.currentToken ? getDatabaseName(ctx.currentToken) : ''; + const t = await getTables.current(tablePath); + + return t.map((table) => ({ + label: table.name, + insertText: table.completion ?? table.name, + command: { id: 'editor.action.triggerSuggest', title: '' }, + kind: CompletionItemKind.Field, + sortText: CompletionItemPriority.High, + range: { + ...ctx.range, + startColumn: ctx.range.endColumn, + endColumn: ctx.range.endColumn, + }, + })); + }, + }, + ]; + +export function getDatabaseName(token: LinkedToken) { + let processedToken = token; + let database = ''; + while (processedToken?.previous && !processedToken.previous.isWhiteSpace()) { + processedToken = processedToken.previous; + database = processedToken.value + database; + } + + if (database.includes(SCHEMA_NAME)) { + database = database.replace(SCHEMA_NAME, ''); + } + + database = database.trim(); + + return database; +} + +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, dataset?: string) { + const tables = await db.lookup(dataset); + return tables; +} diff --git a/public/app/plugins/datasource/mssql/sqlUtil.ts b/public/app/plugins/datasource/mssql/sqlUtil.ts new file mode 100644 index 00000000000..2aa41cb3213 --- /dev/null +++ b/public/app/plugins/datasource/mssql/sqlUtil.ts @@ -0,0 +1,131 @@ +import { isEmpty } from 'lodash'; + +import { RAQBFieldTypes, SQLExpression, SQLQuery } from 'app/features/plugins/sql/types'; +import { haveColumns } from 'app/features/plugins/sql/utils/sql.utils'; + +export function getIcon(type: string): string | undefined { + switch (type) { + case 'datetimeoffset': + case 'date': + case 'datetime2': + case 'smalldatetime': + case 'datetime': + case 'time': + return 'clock-nine'; + case 'bit': + return 'toggle-off'; + case 'tinyint': + case 'smallint': + case 'int': + case 'bigint': + case 'decimal': + case 'numeric': + case 'real': + case 'float': + case 'money': + case 'smallmoney': + return 'calculator-alt'; + case 'char': + case 'varchar': + case 'text': + case 'nchar': + case 'nvarchar': + case 'ntext': + case 'binary': + case 'varbinary': + case 'image': + return 'text'; + default: + return undefined; + } +} + +export function getRAQBType(type: string): RAQBFieldTypes { + switch (type) { + case 'datetimeoffset': + case 'datetime2': + case 'smalldatetime': + case 'datetime': + return 'datetime'; + case 'time': + return 'time'; + case 'date': + return 'date'; + case 'bit': + return 'boolean'; + case 'tinyint': + case 'smallint': + case 'int': + case 'bigint': + case 'decimal': + case 'numeric': + case 'real': + case 'float': + case 'money': + case 'smallmoney': + return 'number'; + case 'char': + case 'varchar': + case 'text': + case 'nchar': + case 'nvarchar': + case 'ntext': + case 'binary': + case 'varbinary': + case 'image': + return 'text'; + default: + return 'text'; + } +} + +export const SCHEMA_NAME = 'dbo'; + +export function toRawSql({ sql, dataset, table }: SQLQuery): string { + let rawQuery = ''; + + // Return early with empty string if there is no sql column + if (!sql || !haveColumns(sql.columns)) { + return rawQuery; + } + + rawQuery += createSelectClause(sql.columns, sql.limit); + + if (dataset && table) { + rawQuery += `FROM ${dataset}.${SCHEMA_NAME}.${table} `; + } + + if (sql.whereString) { + rawQuery += `WHERE ${sql.whereString} `; + } + + if (sql.groupBy?.[0]?.property.name) { + const groupBy = sql.groupBy.map((g) => g.property.name).filter((g) => !isEmpty(g)); + rawQuery += `GROUP BY ${groupBy.join(', ')} `; + } + + if (sql.orderBy?.property.name) { + rawQuery += `ORDER BY ${sql.orderBy.property.name} `; + } + + if (sql.orderBy?.property.name && sql.orderByDirection) { + rawQuery += `${sql.orderByDirection} `; + } + + return rawQuery; +} + +function createSelectClause(sqlColumns: NonNullable, limit?: number): string { + const columns = sqlColumns.map((c) => { + let rawColumn = ''; + if (c.name) { + rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)})`; + } else { + rawColumn += `${c.parameters?.map((p) => `${p.name}`)}`; + } + return rawColumn; + }); + return `SELECT ${isLimit(limit) ? 'TOP(' + limit + ')' : ''} ${columns.join(', ')} `; +} + +const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0; diff --git a/public/app/plugins/datasource/mssql/types.ts b/public/app/plugins/datasource/mssql/types.ts index 58ffb949a28..2f93dd07977 100644 --- a/public/app/plugins/datasource/mssql/types.ts +++ b/public/app/plugins/datasource/mssql/types.ts @@ -1,21 +1,4 @@ -import { DataQuery, DataSourceJsonData } from '@grafana/data'; -import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types'; - -export interface MssqlQueryForInterpolation { - alias?: any; - format?: any; - rawSql?: any; - refId: any; - hide?: any; -} - -export type ResultFormat = 'time_series' | 'table'; - -export interface MssqlQuery extends DataQuery { - alias?: string; - format?: ResultFormat; - rawSql?: any; -} +import { SQLOptions } from 'app/features/plugins/sql/types'; export enum MSSQLAuthenticationType { sqlAuth = 'SQL Server Authentication', @@ -27,14 +10,9 @@ export enum MSSQLEncryptOptions { false = 'false', true = 'true', } -export interface MssqlOptions extends DataSourceJsonData, SQLConnectionLimits { - authenticationType: MSSQLAuthenticationType; - encrypt: MSSQLEncryptOptions; - serverName: string; - sslRootCertFile: string; - tlsSkipVerify: boolean; - url: string; - database: string; - timeInterval: string; - user: string; +export interface MssqlOptions extends SQLOptions { + authenticationType?: MSSQLAuthenticationType; + encrypt?: MSSQLEncryptOptions; + sslRootCertFile?: string; + serverName?: string; } diff --git a/public/app/plugins/datasource/mysql/types.ts b/public/app/plugins/datasource/mysql/types.ts index 88b20f3aade..aadcaa1f84f 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/components/configuration/types'; +import { SQLConnectionLimits } from 'app/features/plugins/sql/types'; export interface MysqlQueryForInterpolation { alias?: any; format?: any; diff --git a/public/app/plugins/datasource/postgres/types.ts b/public/app/plugins/datasource/postgres/types.ts index 7f5cdc1f18d..a07ff7a831a 100644 --- a/public/app/plugins/datasource/postgres/types.ts +++ b/public/app/plugins/datasource/postgres/types.ts @@ -1,5 +1,5 @@ import { DataQuery, DataSourceJsonData } from '@grafana/data'; -import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types'; +import { SQLConnectionLimits } from 'app/features/plugins/sql/types'; export enum PostgresTLSModes { disable = 'disable',