diff --git a/public/app/features/plugins/sql/datasource/SqlDatasource.ts b/public/app/features/plugins/sql/datasource/SqlDatasource.ts index 1128826b9cd..974a8f4c0dd 100644 --- a/public/app/features/plugins/sql/datasource/SqlDatasource.ts +++ b/public/app/features/plugins/sql/datasource/SqlDatasource.ts @@ -164,7 +164,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend) => { const rsp = toDataQueryResponse(res, queries); - return rsp.data[0]; + return rsp.data[0] ?? { fields: [] }; }) ) ); diff --git a/public/app/plugins/datasource/mssql/datasource.test.ts b/public/app/plugins/datasource/mssql/datasource.test.ts index fbd08120245..36a1b17113f 100644 --- a/public/app/plugins/datasource/mssql/datasource.test.ts +++ b/public/app/plugins/datasource/mssql/datasource.test.ts @@ -5,6 +5,7 @@ import { dataFrameToJSON, DataSourceInstanceSettings, dateTime, + FieldType, MetricFindValue, MutableDataFrame, TimeRange, @@ -80,6 +81,133 @@ describe('MSSQLDatasource', () => { }); }); + describe('When runSql returns an empty dataframe', () => { + const response = { + results: { + tempvar: { + refId: 'tempvar', + frames: [], + }, + }, + }; + + beforeEach(async () => { + fetchMock.mockImplementation(() => of(createFetchResponse(response))); + }); + + it('should return an empty array when metricFindQuery is called', async () => { + const query = 'select * from atable'; + const results = await ctx.ds.metricFindQuery(query); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchDatasets is called', async () => { + const results = await ctx.ds.fetchDatasets(); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchTables is called', async () => { + const results = await ctx.ds.fetchTables(); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchFields is called', async () => { + const query: SQLQuery = { + refId: 'refId', + table: 'schema.table', + dataset: 'dataset', + }; + const results = await ctx.ds.fetchFields(query); + expect(results.length).toBe(0); + }); + }); + + describe('When runSql returns a populated dataframe', () => { + it('should return a list of datasets when fetchDatasets is called', async () => { + const fetchDatasetsResponse = { + results: { + datasets: { + refId: 'datasets', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'name', type: FieldType.string, values: ['test1', 'test2', 'test3'] }], + }) + ), + ], + }, + }, + }; + fetchMock.mockImplementation(() => of(createFetchResponse(fetchDatasetsResponse))); + + const results = await ctx.ds.fetchDatasets(); + expect(results.length).toBe(3); + expect(results).toEqual(['test1', 'test2', 'test3']); + }); + + it('should return a list of tables when fetchTables is called', async () => { + const fetchTableResponse = { + results: { + tables: { + refId: 'tables', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'schemaAndName', type: FieldType.string, values: ['test1', 'test2', 'test3'] }], + }) + ), + ], + }, + }, + }; + + fetchMock.mockImplementation(() => of(createFetchResponse(fetchTableResponse))); + + const results = await ctx.ds.fetchTables(); + expect(results.length).toBe(3); + expect(results).toEqual(['test1', 'test2', 'test3']); + }); + + it('should return a list of fields when fetchFields is called', async () => { + const fetchFieldsResponse = { + results: { + columns: { + refId: 'columns', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [ + { name: 'column', type: FieldType.string, values: ['test1', 'test2', 'test3'] }, + { name: 'type', type: FieldType.string, values: ['int', 'char', 'bool'] }, + ], + }) + ), + ], + }, + }, + }; + + fetchMock.mockImplementation(() => of(createFetchResponse(fetchFieldsResponse))); + + const sqlQuery: SQLQuery = { + refId: 'fields', + table: 'table', + dataset: 'dataset', + }; + const results = await ctx.ds.fetchFields(sqlQuery); + expect(results.length).toBe(3); + expect(results[0].label).toBe('test1'); + expect(results[0].value).toBe('test1'); + expect(results[0].type).toBe('int'); + expect(results[1].label).toBe('test2'); + expect(results[1].value).toBe('test2'); + expect(results[1].type).toBe('char'); + expect(results[2].label).toBe('test3'); + expect(results[2].value).toBe('test3'); + expect(results[2].type).toBe('bool'); + }); + }); + describe('When performing metricFindQuery with key, value columns', () => { let results: MetricFindValue[]; const query = 'select * from atable'; diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 0ceacf28e0a..5f85575d54b 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -23,13 +23,13 @@ export class MssqlDatasource extends SqlDatasource { async fetchDatasets(): Promise { const datasets = await this.runSql<{ name: string[] }>(showDatabases(), { refId: 'datasets' }); - return datasets.fields.name.values.toArray().flat(); + return datasets.fields.name?.values.toArray().flat() ?? []; } async fetchTables(dataset?: string): Promise { // We get back the table name with the schema as well. like dbo.table const tables = await this.runSql<{ schemaAndName: string[] }>(getSchemaAndName(dataset), { refId: 'tables' }); - return tables.fields.schemaAndName.values.toArray().flat(); + return tables.fields.schemaAndName?.values.toArray().flat() ?? []; } async fetchFields(query: SQLQuery): Promise { diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index b6d9bc589a7..9e6c749b4d6 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -5,6 +5,7 @@ import { DataQueryRequest, DataSourceInstanceSettings, dateTime, + FieldType, MutableDataFrame, } from '@grafana/data'; import { FetchResponse, setBackendSrv } from '@grafana/runtime'; @@ -17,7 +18,7 @@ import { MySqlDatasource } from '../MySqlDatasource'; import { MySQLOptions } from '../types'; describe('MySQLDatasource', () => { - const setupTextContext = (response: unknown) => { + const setupTestContext = (response: unknown) => { jest.clearAllMocks(); setBackendSrv(backendSrv); const fetchMock = jest.spyOn(backendSrv, 'fetch'); @@ -55,7 +56,7 @@ describe('MySQLDatasource', () => { ], } as unknown as DataQueryRequest; - const { ds, fetchMock } = setupTextContext({}); + const { ds, fetchMock } = setupTestContext({}); await expect(ds.query(options)).toEmitValuesWith((received) => { expect(received[0]).toEqual({ data: [] }); @@ -64,6 +65,132 @@ describe('MySQLDatasource', () => { }); }); + describe('When runSql returns an empty dataframe', () => { + let ds: MySqlDatasource; + const response = { + results: { + tempvar: { + refId: 'tempvar', + frames: [], + }, + }, + }; + + beforeEach(async () => { + ds = setupTestContext(response).ds; + }); + + it('should return an empty array when metricFindQuery is called', async () => { + const query = 'select * from atable'; + const results = await ds.metricFindQuery(query); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchDatasets is called', async () => { + const results = await ds.fetchDatasets(); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchTables is called', async () => { + const results = await ds.fetchTables(); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchFields is called', async () => { + const query: SQLQuery = { + refId: 'refId', + table: 'schema.table', + dataset: 'dataset', + }; + const results = await ds.fetchFields(query); + expect(results.length).toBe(0); + }); + }); + + describe('When runSql returns a populated dataframe', () => { + it('should return a list of datasets when fetchDatasets is called', async () => { + const fetchDatasetsResponse = { + results: { + datasets: { + refId: 'datasets', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'name', type: FieldType.string, values: ['test1', 'test2', 'test3'] }], + }) + ), + ], + }, + }, + }; + const { ds } = setupTestContext(fetchDatasetsResponse); + + const results = await ds.fetchDatasets(); + expect(results.length).toBe(3); + expect(results).toEqual(['test1', 'test2', 'test3']); + }); + + it('should return a list of tables when fetchTables is called', async () => { + const fetchTableResponse = { + results: { + tables: { + refId: 'tables', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'table_name', type: FieldType.string, values: ['test1', 'test2', 'test3'] }], + }) + ), + ], + }, + }, + }; + const { ds } = setupTestContext(fetchTableResponse); + + const results = await ds.fetchTables(); + expect(results.length).toBe(3); + expect(results).toEqual(['test1', 'test2', 'test3']); + }); + + it('should return a list of fields when fetchFields is called', async () => { + const fetchFieldsResponse = { + results: { + fields: { + refId: 'fields', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [ + { name: 'column_name', type: FieldType.string, values: ['test1', 'test2', 'test3'] }, + { name: 'data_type', type: FieldType.string, values: ['int', 'char', 'bool'] }, + ], + }) + ), + ], + }, + }, + }; + const { ds } = setupTestContext(fetchFieldsResponse); + + const sqlQuery: SQLQuery = { + refId: 'fields', + table: 'table', + dataset: 'dataset', + }; + const results = await ds.fetchFields(sqlQuery); + expect(results.length).toBe(3); + expect(results[0].label).toBe('test1'); + expect(results[0].value).toBe('test1'); + expect(results[0].type).toBe('int'); + expect(results[1].label).toBe('test2'); + expect(results[1].value).toBe('test2'); + expect(results[1].type).toBe('char'); + expect(results[2].label).toBe('test3'); + expect(results[2].value).toBe('test3'); + expect(results[2].type).toBe('bool'); + }); + }); + describe('When performing metricFindQuery that returns multiple string fields', () => { const query = 'select * from atable'; const response = { @@ -88,7 +215,7 @@ describe('MySQLDatasource', () => { }; it('should return list of all string field values', async () => { - const { ds } = setupTextContext(response); + const { ds } = setupTestContext(response); const results = await ds.metricFindQuery(query, {}); expect(results.length).toBe(6); @@ -121,7 +248,7 @@ describe('MySQLDatasource', () => { }; it('should return list of all column values', async () => { - const { ds, fetchMock } = setupTextContext(response); + const { ds, fetchMock } = setupTestContext(response); const results = await ds.metricFindQuery(query, { searchFilter: 'aTit' }); expect(fetchMock).toBeCalledTimes(1); @@ -156,7 +283,7 @@ describe('MySQLDatasource', () => { }; it('should return list of all column values', async () => { - const { ds, fetchMock } = setupTextContext(response); + const { ds, fetchMock } = setupTestContext(response); const results = await ds.metricFindQuery(query, {}); expect(fetchMock).toBeCalledTimes(1); @@ -189,7 +316,7 @@ describe('MySQLDatasource', () => { }; it('should return list of as text, value', async () => { - const { ds } = setupTextContext(response); + const { ds } = setupTestContext(response); const results = await ds.metricFindQuery(query, {}); expect(results.length).toBe(3); @@ -224,7 +351,7 @@ describe('MySQLDatasource', () => { }; it('should return list of all field values as text', async () => { - const { ds } = setupTextContext(response); + const { ds } = setupTestContext(response); const results = await ds.metricFindQuery(query, {}); expect(results).toEqual([ @@ -262,7 +389,7 @@ describe('MySQLDatasource', () => { }; it('should return list of unique keys', async () => { - const { ds } = setupTextContext(response); + const { ds } = setupTestContext(response); const results = await ds.metricFindQuery(query, {}); expect(results.length).toBe(1); @@ -274,28 +401,28 @@ describe('MySQLDatasource', () => { describe('When interpolating variables', () => { describe('and value is a string', () => { it('should return an unquoted value', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); expect(ds.interpolateVariable('abc', variable)).toEqual('abc'); }); }); describe('and value is a number', () => { it('should return an unquoted value', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); expect(ds.interpolateVariable(1000, variable)).toEqual(1000); }); }); describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); expect(ds.interpolateVariable(['a', 'b', 'c'], variable)).toEqual("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); variable.multi = true; expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'"); }); @@ -303,7 +430,7 @@ describe('MySQLDatasource', () => { describe('and variable contains single quote', () => { it('should return a quoted value', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); variable.multi = true; expect(ds.interpolateVariable("a'bc", variable)).toEqual("'a''bc'"); }); @@ -311,7 +438,7 @@ describe('MySQLDatasource', () => { describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { - const { ds, variable } = setupTextContext({}); + const { ds, variable } = setupTestContext({}); variable.includeAll = true; expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'"); }); @@ -320,7 +447,7 @@ describe('MySQLDatasource', () => { describe('targetContainsTemplate', () => { it('given query that contains template variable it should return true', () => { - const { ds, templateSrv } = setupTextContext({}); + const { ds, templateSrv } = setupTestContext({}); const rawSql = `SELECT $__timeGroup(createdAt,'$summarize') as time_sec, avg(value) as value, @@ -347,7 +474,7 @@ describe('MySQLDatasource', () => { }); it('given query that only contains global template variable it should return false', () => { - const { ds, templateSrv } = setupTextContext({}); + const { ds, templateSrv } = setupTestContext({}); const rawSql = `SELECT $__timeGroup(createdAt,'$__interval') as time_sec, avg(value) as value, diff --git a/public/app/plugins/datasource/postgres/datasource.test.ts b/public/app/plugins/datasource/postgres/datasource.test.ts index e8b8eef1250..6a6c0789f35 100644 --- a/public/app/plugins/datasource/postgres/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/datasource.test.ts @@ -7,6 +7,7 @@ import { DataQueryResponse, DataSourceInstanceSettings, dateTime, + FieldType, LoadingState, MutableDataFrame, } from '@grafana/data'; @@ -303,6 +304,161 @@ describe('PostgreSQLDatasource', () => { }); }); + describe('When runSql returns an empty dataframe', () => { + let ds: PostgresDatasource; + const response = { + results: { + tempvar: { + refId: 'tempvar', + frames: [], + }, + }, + }; + + beforeEach(async () => { + ds = setupTestContext(response).ds; + }); + + it('should return an empty array when metricFindQuery is called', async () => { + const query = 'select * from atable'; + const results = await ds.metricFindQuery(query); + expect(results.length).toBe(0); + }); + + it('should return an empty array when fetchTables is called', async () => { + const results = await ds.fetchTables(); + expect(results.length).toBe(0); + }); + + it('should return empty string when getVersion is called', async () => { + const results = await ds.getVersion(); + expect(results).toBe(''); + }); + + it('should return undefined when getTimescaleDBVersion is called', async () => { + const results = await ds.getTimescaleDBVersion(); + expect(results).toBe(undefined); + }); + + it('should return an empty array when fetchFields is called', async () => { + const query: SQLQuery = { + refId: 'refId', + table: 'schema.table', + dataset: 'dataset', + }; + const results = await ds.fetchFields(query); + expect(results.length).toBe(0); + }); + }); + + describe('When runSql returns a populated dataframe', () => { + it('should return a list of tables when fetchTables is called', async () => { + const fetchTableResponse = { + results: { + tables: { + refId: 'tables', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'table', type: FieldType.string, values: ['test1', 'test2', 'test3'] }], + }) + ), + ], + }, + }, + }; + + const { ds } = setupTestContext(fetchTableResponse); + + const results = await ds.fetchTables(); + expect(results.length).toBe(3); + expect(results).toEqual(['test1', 'test2', 'test3']); + }); + + it('should return a version string when getVersion is called', async () => { + const fetchVersionResponse = { + results: { + meta: { + refId: 'meta', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'version', type: FieldType.string, values: ['test1'] }], + }) + ), + ], + }, + }, + }; + + const { ds } = setupTestContext(fetchVersionResponse); + + const version = await ds.getVersion(); + expect(version).toBe('test1'); + }); + + it('should return a version string when getTimescaleDBVersion is called', async () => { + const fetchVersionResponse = { + results: { + meta: { + refId: 'meta', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [{ name: 'extversion', type: FieldType.string, values: ['test1'] }], + }) + ), + ], + }, + }, + }; + + const { ds } = setupTestContext(fetchVersionResponse); + + const version = await ds.getTimescaleDBVersion(); + expect(version).toBe('test1'); + }); + + it('should return a list of fields when fetchFields is called', async () => { + const fetchFieldsResponse = { + results: { + columns: { + refId: 'columns', + frames: [ + dataFrameToJSON( + new MutableDataFrame({ + fields: [ + { name: 'column', type: FieldType.string, values: ['test1', 'test2', 'test3'] }, + { name: 'type', type: FieldType.string, values: ['int', 'char', 'bool'] }, + ], + }) + ), + ], + }, + }, + }; + + const { ds } = setupTestContext(fetchFieldsResponse); + + const sqlQuery: SQLQuery = { + refId: 'fields', + table: 'table', + dataset: 'dataset', + }; + const results = await ds.fetchFields(sqlQuery); + expect(results.length).toBe(3); + expect(results[0].label).toBe('test1'); + expect(results[0].value).toBe('test1'); + expect(results[0].type).toBe('int'); + expect(results[1].label).toBe('test2'); + expect(results[1].value).toBe('test2'); + expect(results[1].type).toBe('char'); + expect(results[2].label).toBe('test3'); + expect(results[2].value).toBe('test3'); + expect(results[2].type).toBe('bool'); + }); + }); + describe('When performing metricFindQuery that returns multiple string fields', () => { it('should return list of all string field values', async () => { const query = 'select * from atable'; diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8ec07b794ce..af6d2e97e87 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -24,19 +24,29 @@ export class PostgresDatasource extends SqlDatasource { async getVersion(): Promise { const value = await this.runSql<{ version: number }>(getVersion()); - const results = value.fields.version.values.toArray(); + const results = value.fields.version?.values.toArray(); + + if (!results) { + return ''; + } + return results[0].toString(); } async getTimescaleDBVersion(): Promise { const value = await this.runSql<{ extversion: string }>(getTimescaleDBVersion()); - const results = value.fields.extversion.values.toArray(); + const results = value.fields.extversion?.values.toArray(); + + if (!results) { + return undefined; + } + return results[0]; } async fetchTables(): Promise { const tables = await this.runSql<{ table: string[] }>(showTables(), { refId: 'tables' }); - return tables.fields.table.values.toArray().flat(); + return tables.fields.table?.values.toArray().flat() ?? []; } getSqlLanguageDefinition(db: DB): LanguageDefinition {