datasource.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import { of } from 'rxjs';
  2. import { TestScheduler } from 'rxjs/testing';
  3. import {
  4. dataFrameToJSON,
  5. DataQueryRequest,
  6. DataSourceInstanceSettings,
  7. dateTime,
  8. MutableDataFrame,
  9. } from '@grafana/data';
  10. import { FetchResponse } from '@grafana/runtime';
  11. import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
  12. import { TemplateSrv } from 'app/features/templating/template_srv';
  13. import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer';
  14. import { PostgresDatasource } from '../datasource';
  15. import { PostgresOptions, PostgresQuery } from '../types';
  16. jest.mock('@grafana/runtime', () => ({
  17. ...(jest.requireActual('@grafana/runtime') as unknown as object),
  18. getBackendSrv: () => backendSrv,
  19. }));
  20. jest.mock('@grafana/runtime/src/services', () => ({
  21. ...(jest.requireActual('@grafana/runtime/src/services') as unknown as object),
  22. getBackendSrv: () => backendSrv,
  23. getDataSourceSrv: () => {
  24. return {
  25. getInstanceSettings: () => ({ id: 8674 }),
  26. };
  27. },
  28. }));
  29. describe('PostgreSQLDatasource', () => {
  30. const fetchMock = jest.spyOn(backendSrv, 'fetch');
  31. const setupTestContext = (data: any) => {
  32. jest.clearAllMocks();
  33. fetchMock.mockImplementation(() => of(createFetchResponse(data)));
  34. const instanceSettings = {
  35. jsonData: {
  36. defaultProject: 'testproject',
  37. },
  38. } as unknown as DataSourceInstanceSettings<PostgresOptions>;
  39. const templateSrv: TemplateSrv = new TemplateSrv();
  40. const variable = { ...initialCustomVariableModelState };
  41. const ds = new PostgresDatasource(instanceSettings, templateSrv);
  42. return { ds, templateSrv, variable };
  43. };
  44. // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
  45. const runMarbleTest = (args: {
  46. options: any;
  47. values: { [marble: string]: FetchResponse };
  48. marble: string;
  49. expectedValues: { [marble: string]: any };
  50. expectedMarble: string;
  51. }) => {
  52. const { expectedValues, expectedMarble, options, values, marble } = args;
  53. const scheduler: TestScheduler = new TestScheduler((actual, expected) => {
  54. expect(actual).toEqual(expected);
  55. });
  56. const { ds } = setupTestContext({});
  57. scheduler.run(({ cold, expectObservable }) => {
  58. const source = cold(marble, values);
  59. jest.clearAllMocks();
  60. fetchMock.mockImplementation(() => source);
  61. const result = ds.query(options);
  62. expectObservable(result).toBe(expectedMarble, expectedValues);
  63. });
  64. };
  65. describe('When performing a time series query', () => {
  66. it('should transform response correctly', () => {
  67. const options = {
  68. range: {
  69. from: dateTime(1432288354),
  70. to: dateTime(1432288401),
  71. },
  72. targets: [
  73. {
  74. format: 'time_series',
  75. rawQuery: true,
  76. rawSql: 'select time, metric from grafana_metric',
  77. refId: 'A',
  78. datasource: 'gdev-ds',
  79. },
  80. ],
  81. };
  82. const response = {
  83. results: {
  84. A: {
  85. refId: 'A',
  86. frames: [
  87. dataFrameToJSON(
  88. new MutableDataFrame({
  89. fields: [
  90. { name: 'time', values: [1599643351085] },
  91. { name: 'metric', values: [30.226249741223704], labels: { metric: 'America' } },
  92. ],
  93. meta: {
  94. executedQueryString: 'select time, metric from grafana_metric',
  95. },
  96. })
  97. ),
  98. ],
  99. },
  100. },
  101. };
  102. const values = { a: createFetchResponse(response) };
  103. const marble = '-a|';
  104. const expectedMarble = '-a|';
  105. const expectedValues = {
  106. a: {
  107. data: [
  108. {
  109. fields: [
  110. {
  111. config: {},
  112. entities: {},
  113. name: 'time',
  114. type: 'time',
  115. values: {
  116. buffer: [1599643351085],
  117. },
  118. },
  119. {
  120. config: {},
  121. entities: {},
  122. labels: {
  123. metric: 'America',
  124. },
  125. name: 'metric',
  126. type: 'number',
  127. values: {
  128. buffer: [30.226249741223704],
  129. },
  130. },
  131. ],
  132. length: 1,
  133. meta: {
  134. executedQueryString: 'select time, metric from grafana_metric',
  135. },
  136. name: undefined,
  137. refId: 'A',
  138. },
  139. ],
  140. state: 'Done',
  141. },
  142. };
  143. runMarbleTest({ options, marble, values, expectedMarble, expectedValues });
  144. });
  145. });
  146. describe('When performing a table query', () => {
  147. it('should transform response correctly', () => {
  148. const options = {
  149. range: {
  150. from: dateTime(1432288354),
  151. to: dateTime(1432288401),
  152. },
  153. targets: [
  154. {
  155. format: 'table',
  156. rawQuery: true,
  157. rawSql: 'select time, metric, value from grafana_metric',
  158. refId: 'A',
  159. datasource: 'gdev-ds',
  160. },
  161. ],
  162. };
  163. const response = {
  164. results: {
  165. A: {
  166. refId: 'A',
  167. frames: [
  168. dataFrameToJSON(
  169. new MutableDataFrame({
  170. fields: [
  171. { name: 'time', values: [1599643351085] },
  172. { name: 'metric', values: ['America'] },
  173. { name: 'value', values: [30.226249741223704] },
  174. ],
  175. meta: {
  176. executedQueryString: 'select time, metric, value from grafana_metric',
  177. },
  178. })
  179. ),
  180. ],
  181. },
  182. },
  183. };
  184. const values = { a: createFetchResponse(response) };
  185. const marble = '-a|';
  186. const expectedMarble = '-a|';
  187. const expectedValues = {
  188. a: {
  189. data: [
  190. {
  191. fields: [
  192. {
  193. config: {},
  194. entities: {},
  195. name: 'time',
  196. type: 'time',
  197. values: {
  198. buffer: [1599643351085],
  199. },
  200. },
  201. {
  202. config: {},
  203. entities: {},
  204. name: 'metric',
  205. type: 'string',
  206. values: {
  207. buffer: ['America'],
  208. },
  209. },
  210. {
  211. config: {},
  212. entities: {},
  213. name: 'value',
  214. type: 'number',
  215. values: {
  216. buffer: [30.226249741223704],
  217. },
  218. },
  219. ],
  220. length: 1,
  221. meta: {
  222. executedQueryString: 'select time, metric, value from grafana_metric',
  223. },
  224. name: undefined,
  225. refId: 'A',
  226. },
  227. ],
  228. state: 'Done',
  229. },
  230. };
  231. runMarbleTest({ options, marble, values, expectedMarble, expectedValues });
  232. });
  233. });
  234. describe('When performing a query with hidden target', () => {
  235. it('should return empty result and backendSrv.fetch should not be called', async () => {
  236. const options = {
  237. range: {
  238. from: dateTime(1432288354),
  239. to: dateTime(1432288401),
  240. },
  241. targets: [
  242. {
  243. format: 'table',
  244. rawQuery: true,
  245. rawSql: 'select time, metric, value from grafana_metric',
  246. refId: 'A',
  247. datasource: 'gdev-ds',
  248. hide: true,
  249. },
  250. ],
  251. } as unknown as DataQueryRequest<PostgresQuery>;
  252. const { ds } = setupTestContext({});
  253. await expect(ds.query(options)).toEmitValuesWith((received) => {
  254. expect(received[0]).toEqual({ data: [] });
  255. expect(fetchMock).not.toHaveBeenCalled();
  256. });
  257. });
  258. });
  259. describe('When performing annotationQuery', () => {
  260. let results: any;
  261. const annotationName = 'MyAnno';
  262. const options = {
  263. annotation: {
  264. name: annotationName,
  265. rawQuery: 'select time, title, text, tags from table;',
  266. },
  267. range: {
  268. from: dateTime(1432288354),
  269. to: dateTime(1432288401),
  270. },
  271. };
  272. const response = {
  273. results: {
  274. MyAnno: {
  275. frames: [
  276. dataFrameToJSON(
  277. new MutableDataFrame({
  278. fields: [
  279. { name: 'time', values: [1432288355, 1432288390, 1432288400] },
  280. { name: 'text', values: ['some text', 'some text2', 'some text3'] },
  281. { name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] },
  282. ],
  283. })
  284. ),
  285. ],
  286. },
  287. },
  288. };
  289. beforeEach(async () => {
  290. const { ds } = setupTestContext(response);
  291. results = await ds.annotationQuery(options);
  292. });
  293. it('should return annotation list', async () => {
  294. expect(results.length).toBe(3);
  295. expect(results[0].text).toBe('some text');
  296. expect(results[0].tags[0]).toBe('TagA');
  297. expect(results[0].tags[1]).toBe('TagB');
  298. expect(results[1].tags[0]).toBe('TagB');
  299. expect(results[1].tags[1]).toBe('TagC');
  300. expect(results[2].tags.length).toBe(0);
  301. });
  302. });
  303. describe('When performing metricFindQuery that returns multiple string fields', () => {
  304. it('should return list of all string field values', async () => {
  305. const query = 'select * from atable';
  306. const response = {
  307. results: {
  308. tempvar: {
  309. refId: 'tempvar',
  310. frames: [
  311. dataFrameToJSON(
  312. new MutableDataFrame({
  313. fields: [
  314. { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
  315. { name: 'text', values: ['some text', 'some text2', 'some text3'] },
  316. ],
  317. meta: {
  318. executedQueryString: 'select * from atable',
  319. },
  320. })
  321. ),
  322. ],
  323. },
  324. },
  325. };
  326. const { ds } = setupTestContext(response);
  327. const results = await ds.metricFindQuery(query, {});
  328. expect(results.length).toBe(6);
  329. expect(results[0].text).toBe('aTitle');
  330. expect(results[5].text).toBe('some text3');
  331. });
  332. });
  333. describe('When performing metricFindQuery with $__searchFilter and a searchFilter is given', () => {
  334. it('should return list of all column values', async () => {
  335. const query = "select title from atable where title LIKE '$__searchFilter'";
  336. const response = {
  337. results: {
  338. tempvar: {
  339. refId: 'tempvar',
  340. frames: [
  341. dataFrameToJSON(
  342. new MutableDataFrame({
  343. fields: [
  344. { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
  345. { name: 'text', values: ['some text', 'some text2', 'some text3'] },
  346. ],
  347. meta: {
  348. executedQueryString: 'select * from atable',
  349. },
  350. })
  351. ),
  352. ],
  353. },
  354. },
  355. };
  356. const { ds } = setupTestContext(response);
  357. const results = await ds.metricFindQuery(query, { searchFilter: 'aTit' });
  358. expect(fetchMock).toBeCalledTimes(1);
  359. expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe(
  360. "select title from atable where title LIKE 'aTit%'"
  361. );
  362. expect(results).toEqual([
  363. { text: 'aTitle' },
  364. { text: 'aTitle2' },
  365. { text: 'aTitle3' },
  366. { text: 'some text' },
  367. { text: 'some text2' },
  368. { text: 'some text3' },
  369. ]);
  370. });
  371. });
  372. describe('When performing metricFindQuery with $__searchFilter but no searchFilter is given', () => {
  373. it('should return list of all column values', async () => {
  374. const query = "select title from atable where title LIKE '$__searchFilter'";
  375. const response = {
  376. results: {
  377. tempvar: {
  378. refId: 'tempvar',
  379. frames: [
  380. dataFrameToJSON(
  381. new MutableDataFrame({
  382. fields: [
  383. { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] },
  384. { name: 'text', values: ['some text', 'some text2', 'some text3'] },
  385. ],
  386. meta: {
  387. executedQueryString: 'select * from atable',
  388. },
  389. })
  390. ),
  391. ],
  392. },
  393. },
  394. };
  395. const { ds } = setupTestContext(response);
  396. const results = await ds.metricFindQuery(query, {});
  397. expect(fetchMock).toBeCalledTimes(1);
  398. expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'");
  399. expect(results).toEqual([
  400. { text: 'aTitle' },
  401. { text: 'aTitle2' },
  402. { text: 'aTitle3' },
  403. { text: 'some text' },
  404. { text: 'some text2' },
  405. { text: 'some text3' },
  406. ]);
  407. });
  408. });
  409. describe('When performing metricFindQuery with key, value columns', () => {
  410. it('should return list of as text, value', async () => {
  411. const query = 'select * from atable';
  412. const response = {
  413. results: {
  414. tempvar: {
  415. refId: 'tempvar',
  416. frames: [
  417. dataFrameToJSON(
  418. new MutableDataFrame({
  419. fields: [
  420. { name: '__value', values: ['value1', 'value2', 'value3'] },
  421. { name: '__text', values: ['aTitle', 'aTitle2', 'aTitle3'] },
  422. ],
  423. meta: {
  424. executedQueryString: 'select * from atable',
  425. },
  426. })
  427. ),
  428. ],
  429. },
  430. },
  431. };
  432. const { ds } = setupTestContext(response);
  433. const results = await ds.metricFindQuery(query, {});
  434. expect(results).toEqual([
  435. { text: 'aTitle', value: 'value1' },
  436. { text: 'aTitle2', value: 'value2' },
  437. { text: 'aTitle3', value: 'value3' },
  438. ]);
  439. });
  440. });
  441. describe('When performing metricFindQuery without key, value columns', () => {
  442. it('should return list of all field values as text', async () => {
  443. const query = 'select id, values from atable';
  444. const response = {
  445. results: {
  446. tempvar: {
  447. refId: 'tempvar',
  448. frames: [
  449. dataFrameToJSON(
  450. new MutableDataFrame({
  451. fields: [
  452. { name: 'id', values: [1, 2, 3] },
  453. { name: 'values', values: ['test1', 'test2', 'test3'] },
  454. ],
  455. meta: {
  456. executedQueryString: 'select id, values from atable',
  457. },
  458. })
  459. ),
  460. ],
  461. },
  462. },
  463. };
  464. const { ds } = setupTestContext(response);
  465. const results = await ds.metricFindQuery(query, {});
  466. expect(results).toEqual([
  467. { text: 1 },
  468. { text: 2 },
  469. { text: 3 },
  470. { text: 'test1' },
  471. { text: 'test2' },
  472. { text: 'test3' },
  473. ]);
  474. });
  475. });
  476. describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => {
  477. it('should return list of unique keys', async () => {
  478. const query = 'select * from atable';
  479. const response = {
  480. results: {
  481. tempvar: {
  482. refId: 'tempvar',
  483. frames: [
  484. dataFrameToJSON(
  485. new MutableDataFrame({
  486. fields: [
  487. { name: '__text', values: ['aTitle', 'aTitle', 'aTitle'] },
  488. { name: '__value', values: ['same', 'same', 'diff'] },
  489. ],
  490. meta: {
  491. executedQueryString: 'select * from atable',
  492. },
  493. })
  494. ),
  495. ],
  496. },
  497. },
  498. };
  499. const { ds } = setupTestContext(response);
  500. const results = await ds.metricFindQuery(query, {});
  501. expect(results).toEqual([{ text: 'aTitle', value: 'same' }]);
  502. });
  503. });
  504. describe('When interpolating variables', () => {
  505. describe('and value is a string', () => {
  506. it('should return an unquoted value', () => {
  507. const { ds, variable } = setupTestContext({});
  508. expect(ds.interpolateVariable('abc', variable)).toEqual('abc');
  509. });
  510. });
  511. describe('and value is a number', () => {
  512. it('should return an unquoted value', () => {
  513. const { ds, variable } = setupTestContext({});
  514. expect(ds.interpolateVariable(1000 as unknown as string, variable)).toEqual(1000);
  515. });
  516. });
  517. describe('and value is an array of strings', () => {
  518. it('should return comma separated quoted values', () => {
  519. const { ds, variable } = setupTestContext({});
  520. expect(ds.interpolateVariable(['a', 'b', 'c'], variable)).toEqual("'a','b','c'");
  521. });
  522. });
  523. describe('and variable allows multi-value and is a string', () => {
  524. it('should return a quoted value', () => {
  525. const { ds, variable } = setupTestContext({});
  526. variable.multi = true;
  527. expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'");
  528. });
  529. });
  530. describe('and variable contains single quote', () => {
  531. it('should return a quoted value', () => {
  532. const { ds, variable } = setupTestContext({});
  533. variable.multi = true;
  534. expect(ds.interpolateVariable("a'bc", variable)).toEqual("'a''bc'");
  535. expect(ds.interpolateVariable("a'b'c", variable)).toEqual("'a''b''c'");
  536. });
  537. });
  538. describe('and variable allows all and is a string', () => {
  539. it('should return a quoted value', () => {
  540. const { ds, variable } = setupTestContext({});
  541. variable.includeAll = true;
  542. expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'");
  543. });
  544. });
  545. });
  546. describe('targetContainsTemplate', () => {
  547. it('given query that contains template variable it should return true', () => {
  548. const rawSql = `SELECT
  549. $__timeGroup("createdAt",'$summarize'),
  550. avg(value) as "value",
  551. hostname as "metric"
  552. FROM
  553. grafana_metric
  554. WHERE
  555. $__timeFilter("createdAt") AND
  556. measurement = 'logins.count' AND
  557. hostname IN($host)
  558. GROUP BY time, metric
  559. ORDER BY time`;
  560. const query = {
  561. rawSql,
  562. rawQuery: true,
  563. };
  564. const { templateSrv, ds } = setupTestContext({});
  565. templateSrv.init([
  566. { type: 'query', name: 'summarize', current: { value: '1m' } },
  567. { type: 'query', name: 'host', current: { value: 'a' } },
  568. ]);
  569. expect(ds.targetContainsTemplate(query)).toBeTruthy();
  570. });
  571. it('given query that only contains global template variable it should return false', () => {
  572. const rawSql = `SELECT
  573. $__timeGroup("createdAt",'$__interval'),
  574. avg(value) as "value",
  575. hostname as "metric"
  576. FROM
  577. grafana_metric
  578. WHERE
  579. $__timeFilter("createdAt") AND
  580. measurement = 'logins.count'
  581. GROUP BY time, metric
  582. ORDER BY time`;
  583. const query = {
  584. rawSql,
  585. rawQuery: true,
  586. };
  587. const { templateSrv, ds } = setupTestContext({});
  588. templateSrv.init([
  589. { type: 'query', name: 'summarize', current: { value: '1m' } },
  590. { type: 'query', name: 'host', current: { value: 'a' } },
  591. ]);
  592. expect(ds.targetContainsTemplate(query)).toBeFalsy();
  593. });
  594. });
  595. });
  596. const createFetchResponse = <T>(data: T): FetchResponse<T> => ({
  597. data,
  598. status: 200,
  599. url: 'http://localhost:3000/api/query',
  600. config: { url: 'http://localhost:3000/api/query' },
  601. type: 'basic',
  602. statusText: 'Ok',
  603. redirected: false,
  604. headers: {} as unknown as Headers,
  605. ok: true,
  606. });