SQLGenerator.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import { TemplateSrv } from 'app/features/templating/template_srv';
  2. import {
  3. aggregationvariable,
  4. labelsVariable,
  5. metricVariable,
  6. namespaceVariable,
  7. } from '../__mocks__/CloudWatchDataSource';
  8. import {
  9. createFunctionWithParameter,
  10. createArray,
  11. createOperator,
  12. createGroupBy,
  13. createFunction,
  14. createProperty,
  15. } from '../__mocks__/sqlUtils';
  16. import { QueryEditorExpressionType } from '../expressions';
  17. import { SQLExpression } from '../types';
  18. import SQLGenerator from './SQLGenerator';
  19. describe('SQLGenerator', () => {
  20. let baseQuery: SQLExpression = {
  21. select: createFunctionWithParameter('SUM', ['CPUUtilization']),
  22. from: createFunctionWithParameter('SCHEMA', ['AWS/EC2']),
  23. orderByDirection: 'DESC',
  24. };
  25. describe('mandatory fields check', () => {
  26. it('should return undefined if metric and aggregation is missing', () => {
  27. expect(
  28. new SQLGenerator().expressionToSqlQuery({
  29. from: createFunctionWithParameter('SCHEMA', ['AWS/EC2']),
  30. })
  31. ).toBeUndefined();
  32. });
  33. it('should return undefined if aggregation is missing', () => {
  34. expect(
  35. new SQLGenerator().expressionToSqlQuery({
  36. from: createFunctionWithParameter('SCHEMA', []),
  37. })
  38. ).toBeUndefined();
  39. });
  40. });
  41. it('should return query if mandatory fields are provided', () => {
  42. expect(new SQLGenerator().expressionToSqlQuery(baseQuery)).not.toBeUndefined();
  43. });
  44. describe('select', () => {
  45. it('should use statistic and metric name', () => {
  46. const select = createFunctionWithParameter('COUNT', ['BytesPerSecond']);
  47. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, select })).toEqual(
  48. `SELECT COUNT(BytesPerSecond) FROM SCHEMA("AWS/EC2")`
  49. );
  50. });
  51. it('should wrap in double quotes if metric name contains illegal characters ', () => {
  52. const select = createFunctionWithParameter('COUNT', ['Bytes-Per-Second']);
  53. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, select })).toEqual(
  54. `SELECT COUNT("Bytes-Per-Second") FROM SCHEMA("AWS/EC2")`
  55. );
  56. });
  57. });
  58. describe('from', () => {
  59. describe('with schema contraint', () => {
  60. it('should handle schema without dimensions', () => {
  61. const from = createFunctionWithParameter('SCHEMA', ['AWS/MQ']);
  62. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual(
  63. `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ")`
  64. );
  65. });
  66. it('should handle schema with dimensions', () => {
  67. const from = createFunctionWithParameter('SCHEMA', ['AWS/MQ', 'InstanceId', 'InstanceType']);
  68. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual(
  69. `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ", InstanceId, InstanceType)`
  70. );
  71. });
  72. it('should handle schema with dimensions that has special characters', () => {
  73. const from = createFunctionWithParameter('SCHEMA', [
  74. 'AWS/MQ',
  75. 'Instance Id',
  76. 'Instance.Type',
  77. 'Instance-Group',
  78. ]);
  79. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual(
  80. `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ", "Instance Id", "Instance.Type", "Instance-Group")`
  81. );
  82. });
  83. });
  84. describe('without schema', () => {
  85. it('should use the specified namespace', () => {
  86. const from = createProperty('AWS/MQ');
  87. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual(
  88. `SELECT SUM(CPUUtilization) FROM "AWS/MQ"`
  89. );
  90. });
  91. });
  92. });
  93. function assertQueryEndsWith(rest: Partial<SQLExpression>, expectedFilter: string) {
  94. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, ...rest })).toEqual(
  95. `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/EC2") ${expectedFilter}`
  96. );
  97. }
  98. describe('filter', () => {
  99. it('should not add WHERE clause in case its empty', () => {
  100. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('WHERE');
  101. });
  102. it('should not add WHERE clause when there is no filter conditions', () => {
  103. const where = createArray([]);
  104. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE');
  105. });
  106. // TODO: We should handle this scenario
  107. it.skip('should not add WHERE clause when the operator is incomplete', () => {
  108. const where = createArray([createOperator('Instance-Id', '=')]);
  109. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE');
  110. });
  111. it('should handle one top level filter with AND', () => {
  112. const where = createArray([createOperator('Instance-Id', '=', 'I-123')]);
  113. assertQueryEndsWith({ where }, `WHERE "Instance-Id" = 'I-123'`);
  114. });
  115. it('should handle one top level filter with OR', () => {
  116. assertQueryEndsWith(
  117. { where: createArray([createOperator('InstanceId', '=', 'I-123')]) },
  118. `WHERE InstanceId = 'I-123'`
  119. );
  120. });
  121. it('should handle multiple top level filters combined with AND', () => {
  122. const filter = createArray(
  123. [createOperator('InstanceId', '=', 'I-123'), createOperator('Instance-Id', '!=', 'I-456')],
  124. QueryEditorExpressionType.And
  125. );
  126. assertQueryEndsWith({ where: filter }, `WHERE InstanceId = 'I-123' AND "Instance-Id" != 'I-456'`);
  127. });
  128. it('should handle multiple top level filters combined with OR', () => {
  129. const filter = createArray(
  130. [createOperator('InstanceId', '=', 'I-123'), createOperator('InstanceId', '!=', 'I-456')],
  131. QueryEditorExpressionType.Or
  132. );
  133. assertQueryEndsWith({ where: filter }, `WHERE InstanceId = 'I-123' OR InstanceId != 'I-456'`);
  134. });
  135. it('should handle one top level filters with one nested filter', () => {
  136. const filter = createArray(
  137. [
  138. createOperator('InstanceId', '=', 'I-123'),
  139. createArray([createOperator('InstanceId', '!=', 'I-456')], QueryEditorExpressionType.And),
  140. ],
  141. QueryEditorExpressionType.And
  142. );
  143. assertQueryEndsWith({ where: filter }, `WHERE InstanceId = 'I-123' AND InstanceId != 'I-456'`);
  144. });
  145. it('should handle one top level filter with two nested filters combined with AND', () => {
  146. const filter = createArray(
  147. [
  148. createOperator('Instance.Type', '=', 'I-123'),
  149. createArray(
  150. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  151. QueryEditorExpressionType.And
  152. ),
  153. ],
  154. QueryEditorExpressionType.And
  155. );
  156. // In this scenario, the parenthesis are redundant. However, they're not doing any harm and it would be really complicated to remove them
  157. assertQueryEndsWith(
  158. { where: filter },
  159. `WHERE "Instance.Type" = 'I-123' AND (InstanceId != 'I-456' AND Type != 'some-type')`
  160. );
  161. });
  162. it('should handle one top level filter with two nested filters combined with OR', () => {
  163. const filter = createArray(
  164. [
  165. createOperator('InstanceId', '=', 'I-123'),
  166. createArray(
  167. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  168. QueryEditorExpressionType.Or
  169. ),
  170. ],
  171. QueryEditorExpressionType.And
  172. );
  173. assertQueryEndsWith(
  174. { where: filter },
  175. `WHERE InstanceId = 'I-123' AND (InstanceId != 'I-456' OR Type != 'some-type')`
  176. );
  177. });
  178. it('should handle two top level filters with two nested filters combined with AND', () => {
  179. const filter = createArray(
  180. [
  181. createArray(
  182. [createOperator('InstanceId', '=', 'I-123'), createOperator('Type', '!=', 'some-type')],
  183. QueryEditorExpressionType.And
  184. ),
  185. createArray(
  186. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  187. QueryEditorExpressionType.Or
  188. ),
  189. ],
  190. QueryEditorExpressionType.And
  191. );
  192. assertQueryEndsWith(
  193. { where: filter },
  194. `WHERE (InstanceId = 'I-123' AND Type != 'some-type') AND (InstanceId != 'I-456' OR Type != 'some-type')`
  195. );
  196. });
  197. it('should handle two top level filters with two nested filters combined with OR', () => {
  198. const filter = createArray(
  199. [
  200. createArray(
  201. [createOperator('InstanceId', '=', 'I-123'), createOperator('Type', '!=', 'some-type')],
  202. QueryEditorExpressionType.Or
  203. ),
  204. createArray(
  205. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  206. QueryEditorExpressionType.Or
  207. ),
  208. ],
  209. QueryEditorExpressionType.Or
  210. );
  211. assertQueryEndsWith(
  212. { where: filter },
  213. `WHERE (InstanceId = 'I-123' OR Type != 'some-type') OR (InstanceId != 'I-456' OR Type != 'some-type')`
  214. );
  215. });
  216. it('should handle three top level filters with one nested filters combined with OR', () => {
  217. const filter = createArray(
  218. [
  219. createArray([createOperator('InstanceId', '=', 'I-123')], QueryEditorExpressionType.Or),
  220. createArray([createOperator('Type', '!=', 'some-type')], QueryEditorExpressionType.Or),
  221. createArray([createOperator('InstanceId', '!=', 'I-456')], QueryEditorExpressionType.Or),
  222. ],
  223. QueryEditorExpressionType.Or
  224. );
  225. assertQueryEndsWith(
  226. { where: filter },
  227. `WHERE InstanceId = 'I-123' OR Type != 'some-type' OR InstanceId != 'I-456'`
  228. );
  229. });
  230. it('should handle three top level filters with one nested filters combined with AND', () => {
  231. const filter = createArray(
  232. [
  233. createArray([createOperator('InstanceId', '=', 'I-123')], QueryEditorExpressionType.Or),
  234. createArray([createOperator('Type', '!=', 'some-type')], QueryEditorExpressionType.Or),
  235. createArray([createOperator('InstanceId', '!=', 'I-456')], QueryEditorExpressionType.Or),
  236. ],
  237. QueryEditorExpressionType.And
  238. );
  239. assertQueryEndsWith(
  240. { where: filter },
  241. `WHERE InstanceId = 'I-123' AND Type != 'some-type' AND InstanceId != 'I-456'`
  242. );
  243. });
  244. });
  245. describe('group by', () => {
  246. it('should not add GROUP BY clause in case its empty', () => {
  247. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('GROUP BY');
  248. });
  249. it('should handle single label', () => {
  250. const groupBy = createArray([createGroupBy('InstanceId')], QueryEditorExpressionType.And);
  251. assertQueryEndsWith({ groupBy }, `GROUP BY InstanceId`);
  252. });
  253. it('should handle multiple label', () => {
  254. const groupBy = createArray(
  255. [createGroupBy('InstanceId'), createGroupBy('Type'), createGroupBy('Group')],
  256. QueryEditorExpressionType.And
  257. );
  258. assertQueryEndsWith({ groupBy }, `GROUP BY InstanceId, Type, Group`);
  259. });
  260. });
  261. describe('order by', () => {
  262. it('should not add ORDER BY clause in case its empty', () => {
  263. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('ORDER BY');
  264. });
  265. it('should handle SUM ASC', () => {
  266. const orderBy = createFunction('SUM');
  267. assertQueryEndsWith({ orderBy, orderByDirection: 'ASC' }, `ORDER BY SUM() ASC`);
  268. });
  269. it('should handle SUM ASC', () => {
  270. const orderBy = createFunction('SUM');
  271. assertQueryEndsWith({ orderBy, orderByDirection: 'ASC' }, `ORDER BY SUM() ASC`);
  272. });
  273. it('should handle COUNT DESC', () => {
  274. const orderBy = createFunction('COUNT');
  275. assertQueryEndsWith({ orderBy, orderByDirection: 'DESC' }, `ORDER BY COUNT() DESC`);
  276. });
  277. });
  278. describe('limit', () => {
  279. it('should not add LIMIT clause in case its empty', () => {
  280. expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('LIMIT');
  281. });
  282. it('should be added in case its specified', () => {
  283. assertQueryEndsWith({ limit: 10 }, `LIMIT 10`);
  284. });
  285. });
  286. describe('full query', () => {
  287. it('should not add LIMIT clause in case its empty', () => {
  288. let query: SQLExpression = {
  289. select: createFunctionWithParameter('COUNT', ['DroppedBytes']),
  290. from: createFunctionWithParameter('SCHEMA', ['AWS/MQ', 'InstanceId', 'Instance-Group']),
  291. where: createArray(
  292. [
  293. createArray(
  294. [createOperator('InstanceId', '=', 'I-123'), createOperator('Type', '!=', 'some-type')],
  295. QueryEditorExpressionType.Or
  296. ),
  297. createArray(
  298. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  299. QueryEditorExpressionType.Or
  300. ),
  301. ],
  302. QueryEditorExpressionType.And
  303. ),
  304. groupBy: createArray([createGroupBy('InstanceId'), createGroupBy('InstanceType')]),
  305. orderBy: createFunction('COUNT'),
  306. orderByDirection: 'DESC',
  307. limit: 100,
  308. };
  309. expect(new SQLGenerator().expressionToSqlQuery(query)).toEqual(
  310. `SELECT COUNT(DroppedBytes) FROM SCHEMA("AWS/MQ", InstanceId, "Instance-Group") WHERE (InstanceId = 'I-123' OR Type != 'some-type') AND (InstanceId != 'I-456' OR Type != 'some-type') GROUP BY InstanceId, InstanceType ORDER BY COUNT() DESC LIMIT 100`
  311. );
  312. });
  313. });
  314. describe('using variables', () => {
  315. const templateService = new TemplateSrv();
  316. templateService.init([metricVariable, namespaceVariable, labelsVariable, aggregationvariable]);
  317. it('should interpolate variables correctly', () => {
  318. let query: SQLExpression = {
  319. select: createFunctionWithParameter('$aggregation', ['$metric']),
  320. from: createFunctionWithParameter('SCHEMA', ['$namespace', '$labels']),
  321. where: createArray(
  322. [
  323. createArray(
  324. [createOperator('InstanceId', '=', 'I-123'), createOperator('Type', '!=', 'some-type')],
  325. QueryEditorExpressionType.Or
  326. ),
  327. createArray(
  328. [createOperator('InstanceId', '!=', 'I-456'), createOperator('Type', '!=', 'some-type')],
  329. QueryEditorExpressionType.Or
  330. ),
  331. ],
  332. QueryEditorExpressionType.And
  333. ),
  334. groupBy: createArray([createGroupBy('$labels')]),
  335. orderBy: createFunction('$aggregation'),
  336. orderByDirection: 'DESC',
  337. limit: 100,
  338. };
  339. expect(new SQLGenerator(templateService).expressionToSqlQuery(query)).toEqual(
  340. `SELECT $aggregation($metric) FROM SCHEMA(\"$namespace\", $labels) WHERE (InstanceId = 'I-123' OR Type != 'some-type') AND (InstanceId != 'I-456' OR Type != 'some-type') GROUP BY $labels ORDER BY $aggregation() DESC LIMIT 100`
  341. );
  342. });
  343. });
  344. });