diff --git a/.betterer.results b/.betterer.results index e18424ab5a8..130ed780c5b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7735,27 +7735,6 @@ exports[`better eslint`] = { [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/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/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/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/plugins/datasource/mssql/MSSqlMetaQuery.ts b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts new file mode 100644 index 00000000000..089e13d6a3f --- /dev/null +++ b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts @@ -0,0 +1,15 @@ +export function showDatabases() { + // Return only user defined databases + return `SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb');`; +} + +export function showTables(dataset?: string) { + return `SELECT TABLE_NAME + FROM [${dataset}].INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE'`; +} + +export function getSchema(table?: string) { + return `SELECT COLUMN_NAME as 'column',DATA_TYPE as 'type' + FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='${table}';`; +} diff --git a/public/app/plugins/datasource/mssql/MSSqlQueryModel.ts b/public/app/plugins/datasource/mssql/MSSqlQueryModel.ts new file mode 100644 index 00000000000..9e8f569e483 --- /dev/null +++ b/public/app/plugins/datasource/mssql/MSSqlQueryModel.ts @@ -0,0 +1,24 @@ +import { ScopedVars } from '@grafana/data'; +import { TemplateSrv } from '@grafana/runtime'; +import { applyQueryDefaults } from 'app/features/plugins/sql/defaults'; +import { SQLQuery, SqlQueryModel } from 'app/features/plugins/sql/types'; + +export class MSSqlQueryModel implements SqlQueryModel { + target: SQLQuery; + templateSrv?: TemplateSrv; + scopedVars?: ScopedVars; + + constructor(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) { + this.target = applyQueryDefaults(target); + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + } + + interpolate() { + return this.templateSrv?.replace(this.target.rawSql, this.scopedVars, this.target.format) || ''; + } + + quoteLiteral(value: string) { + return "'" + value.replace(/'/g, "''") + "'"; + } +} diff --git a/public/app/plugins/datasource/mssql/specs/datasource.test.ts b/public/app/plugins/datasource/mssql/datasource.test.ts similarity index 76% rename from public/app/plugins/datasource/mssql/specs/datasource.test.ts rename to public/app/plugins/datasource/mssql/datasource.test.ts index d0c9feaa8b8..c6a0cb0f2fc 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mssql/datasource.test.ts @@ -1,85 +1,47 @@ import { of } from 'rxjs'; import { createFetchResponse } from 'test/helpers/createFetchResponse'; -import { dataFrameToJSON, dateTime, MetricFindValue, MutableDataFrame } from '@grafana/data'; +import { + dataFrameToJSON, + DataSourceInstanceSettings, + dateTime, + MetricFindValue, + MutableDataFrame, + TimeRange, +} from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; +import { SQLOptions, SQLQuery } from 'app/features/plugins/sql/types'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer'; -import { MssqlDatasource } from '../datasource'; +import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; + +import { MssqlDatasource } from './datasource'; jest.mock('@grafana/runtime', () => ({ - ...(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 +80,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 +102,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 +117,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 +143,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 +161,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 +182,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 +209,10 @@ describe('MSSQLDatasource', () => { }, }, }; - const time = { + const time: TimeRange = { from: dateTime(1521545610656), to: dateTime(1521546251185), + raw: { from: '1521545610656', to: '1521546251185' }, }; beforeEach(() => { @@ -268,10 +231,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 +273,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 +286,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 +312,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..8de1d6f0aa7 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -1,191 +1,91 @@ -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, SQLOptions } 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, toRawSql } from './sqlUtil'; -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.metricFindQuery(showDatabases(), {}); + return datasets.map((t) => t.text); + } + + async fetchTables(dataset?: string): Promise { + const tables = await this.metricFindQuery(showTables(dataset), {}); + return tables.map((t) => t.text); + } + + async fetchFields(query: SQLQuery): Promise { + const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.table)); + 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: (query: SQLQuery) => 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}.` })); + } 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 2426595a230..9940f7d30aa 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -1,34 +1,10 @@ import { DataSourcePlugin } from '@grafana/data'; +import { SqlQueryEditor } from 'app/features/plugins/sql/components/QueryEditor'; +import { SQLQuery } from 'app/features/plugins/sql/types'; import { MssqlConfigCtrl } from './config_ctrl'; import { MssqlDatasource } from './datasource'; -import { MssqlQueryCtrl } from './query_ctrl'; -import { MssqlQuery } 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) - .setConfigCtrl(MssqlConfigCtrl) - .setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl); +export const plugin = new DataSourcePlugin(MssqlDatasource) + .setQueryEditor(SqlQueryEditor) + .setConfigCtrl(MssqlConfigCtrl); 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..9b4b9fd0392 --- /dev/null +++ b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts @@ -0,0 +1,212 @@ +// import { useCallback } from 'react'; + +import { + ColumnDefinition, + CompletionItemKind, + CompletionItemPriority, + LanguageCompletionProvider, + LinkedToken, + StatementPlacementProvider, + // StatementPosition, + 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'; + +interface CompletionProviderGetterArgs { + getColumns: React.MutableRefObject<(t: SQLQuery) => Promise>; + getTables: React.MutableRefObject<(d?: string) => Promise>; + // getTableSchema: React.MutableRefObject<(p: string, d: string, t: 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) { + tablePath += processedToken.next.value; + processedToken = processedToken.next; + } + + return tablePath; + }, + }, + + columns: { + resolve: async (t: string) => { + // TODO - seems like a limitation in experimental since we may need database and table to get columns + // use . as delimiter? + // const cols = await getColumns({ table: t } as SQLQuery); + return await getColumns.current({ table: t, refId: 'A' }); + }, + }, + supportedFunctions: () => AGGREGATE_FNS, + supportedOperators: () => OPERATORS, + customSuggestionKinds: customSuggestionKinds(getTables, getColumns), + customStatementPlacement, + }); + +export enum CustomStatementPlacement { + AfterDataset = 'afterDataset', +} + +export enum CustomSuggestionKind { + TablesWithinDataset = 'tablesWithinDataset', + Partition = 'partition', +} + +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, '.')) || + // (currentToken?.value === '`' && currentToken?.previous?.is(TokenType.Delimiter, '.')) || + (currentToken?.isNumber() && currentToken.value.endsWith('.')) // number with dot at the end like "projectname-21342." + // (currentToken?.value === '`' && isTypingTableIn(currentToken)) + ); + }, + }, + // TODO - remove - frin big query + // Overriding default behaviour of AfterFrom resolver + // { + // id: StatementPosition.AfterFrom, + // overrideDefault: true, + // resolve: (currentToken) => { + // const untilFrom = currentToken?.getPreviousUntil(TokenType.Keyword, [], 'from'); + // if (!untilFrom) { + // return false; + // } + // let q = ''; + // for (let i = untilFrom?.length - 1; i >= 0; i--) { + // q += untilFrom[i].value; + // } + + // return q.startsWith('`') && q.endsWith('`'); + // }, + // }, +]; + +export const customSuggestionKinds: ( + getTables: CompletionProviderGetterArgs['getTables'], + getFields: CompletionProviderGetterArgs['getColumns'] + // getTableSchema: CompletionProviderGetterArgs['getTableSchema'] +) => SuggestionKindProvider = (getTables) => () => + [ + { + 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) => ({ + 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, + }, + })); + }, + }, + // { + // id: CustomSuggestionKind.TablesWithinDataset, + // applyTo: [StatementPosition.AfterFrom], + // suggestionsResolver: async (ctx) => { + // console.log('after from'); + // const tablePath = ctx.currentToken ? getTablePath(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 getTablePath(token: LinkedToken) { + let processedToken = token; + let tablePath = ''; + while (processedToken?.previous && !processedToken.previous.isWhiteSpace()) { + processedToken = processedToken.previous; + tablePath = processedToken.value + tablePath; + } + + tablePath = tablePath.trim(); + + // TODO - remove - frin big query + + // if (tablePath.startsWith('`')) { + // tablePath = tablePath.slice(1); + // } + + // if (tablePath.endsWith('`')) { + // tablePath = tablePath.slice(0, -1); + // } + + return tablePath; +} + +// TODO - not sure if we need this +// function isTypingTableIn(token: LinkedToken | null, l?: boolean) { +// if (!token) { +// return false; +// } +// const tokens = token.getPreviousUntil(TokenType.Keyword, [], 'from'); +// if (!tokens) { +// return false; +// } + +// let path = ''; +// for (let i = tokens.length - 1; i >= 0; i--) { +// path += tokens[i].value; +// } + +// // TODO - remove - frin big query +// // if (path.startsWith('`')) { +// // path = path.slice(1); +// // } + +// return path.split('.').length === 2; +// } + +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..3aaff45f41a --- /dev/null +++ b/public/app/plugins/datasource/mssql/sqlUtil.ts @@ -0,0 +1,129 @@ +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 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}.dbo.${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 deleted file mode 100644 index 8359e566f32..00000000000 --- a/public/app/plugins/datasource/mssql/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DataQuery, DataSourceJsonData } from '@grafana/data'; - -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; -} - -export interface MssqlOptions extends DataSourceJsonData { - timeInterval: string; -}