buildQuery.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. import { AdhocColumn, QueryMode, VizType } from '@superset-ui/core';
  20. import buildQuery from '../src/buildQuery';
  21. import { TableChartFormData } from '../src/types';
  22. const basicFormData: TableChartFormData = {
  23. viz_type: VizType.Table,
  24. datasource: '11__table',
  25. query_mode: QueryMode.Aggregate,
  26. groupby: ['state'],
  27. metrics: ['count'],
  28. };
  29. const createAdhocColumn = (
  30. sqlExpression: string,
  31. label: string,
  32. ): AdhocColumn => ({
  33. sqlExpression,
  34. label,
  35. expressionType: 'SQL',
  36. });
  37. describe('plugin-chart-ag-grid-table', () => {
  38. describe('buildQuery - sort mapping for server pagination', () => {
  39. it('should map string column colId to backend identifier', () => {
  40. const query = buildQuery(
  41. {
  42. ...basicFormData,
  43. server_pagination: true,
  44. },
  45. {
  46. ownState: {
  47. sortBy: [{ key: 'state', desc: false }],
  48. },
  49. },
  50. ).queries[0];
  51. expect(query.orderby).toEqual([['state', true]]);
  52. });
  53. it('should map AdhocColumn colId by sqlExpression', () => {
  54. const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
  55. const query = buildQuery(
  56. {
  57. ...basicFormData,
  58. server_pagination: true,
  59. groupby: [adhocColumn],
  60. },
  61. {
  62. ownState: {
  63. sortBy: [{ key: 'degree_type', desc: false }],
  64. },
  65. },
  66. ).queries[0];
  67. expect(query.orderby).toEqual([['degree_type', true]]);
  68. });
  69. it('should map AdhocColumn colId by label', () => {
  70. const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
  71. const query = buildQuery(
  72. {
  73. ...basicFormData,
  74. server_pagination: true,
  75. groupby: [adhocColumn],
  76. },
  77. {
  78. ownState: {
  79. sortBy: [{ key: 'Highest Degree', desc: false }],
  80. },
  81. },
  82. ).queries[0];
  83. expect(query.orderby).toEqual([['degree_type', true]]);
  84. });
  85. it('should map string metric colId to backend identifier', () => {
  86. const query = buildQuery(
  87. {
  88. ...basicFormData,
  89. server_pagination: true,
  90. metrics: ['SUM(revenue)'],
  91. },
  92. {
  93. ownState: {
  94. sortBy: [{ key: 'SUM(revenue)', desc: true }],
  95. },
  96. },
  97. ).queries[0];
  98. expect(query.orderby).toEqual([['SUM(revenue)', false]]);
  99. });
  100. it('should map percent metric with % prefix', () => {
  101. const query = buildQuery(
  102. {
  103. ...basicFormData,
  104. server_pagination: true,
  105. metrics: ['revenue'],
  106. percent_metrics: ['revenue'],
  107. },
  108. {
  109. ownState: {
  110. sortBy: [{ key: '%revenue', desc: false }],
  111. },
  112. },
  113. ).queries[0];
  114. expect(query.orderby).toEqual([['revenue', true]]);
  115. });
  116. it('should handle desc sort direction correctly', () => {
  117. const query = buildQuery(
  118. {
  119. ...basicFormData,
  120. server_pagination: true,
  121. },
  122. {
  123. ownState: {
  124. sortBy: [{ key: 'state', desc: true }],
  125. },
  126. },
  127. ).queries[0];
  128. expect(query.orderby).toEqual([['state', false]]);
  129. });
  130. });
  131. describe('buildQuery - CSV export with sortModel', () => {
  132. it('should use sortModel for download queries', () => {
  133. const query = buildQuery(
  134. {
  135. ...basicFormData,
  136. result_format: 'csv',
  137. },
  138. {
  139. ownState: {
  140. sortModel: [{ colId: 'state', sort: 'asc' }],
  141. sortBy: [{ key: 'other', desc: false }],
  142. },
  143. },
  144. ).queries[0];
  145. expect(query.orderby).toEqual([
  146. ['state', true],
  147. ['count', false],
  148. ]);
  149. });
  150. it('should map sortModel with desc direction', () => {
  151. const query = buildQuery(
  152. {
  153. ...basicFormData,
  154. result_format: 'csv',
  155. },
  156. {
  157. ownState: {
  158. sortModel: [{ colId: 'state', sort: 'desc' }],
  159. },
  160. },
  161. ).queries[0];
  162. expect(query.orderby?.[0]).toEqual(['state', false]);
  163. });
  164. it('should handle multi-column sort from sortModel', () => {
  165. const query = buildQuery(
  166. {
  167. ...basicFormData,
  168. groupby: ['state', 'city'],
  169. result_format: 'csv',
  170. },
  171. {
  172. ownState: {
  173. sortModel: [
  174. { colId: 'state', sort: 'asc', sortIndex: 0 },
  175. { colId: 'city', sort: 'desc', sortIndex: 1 },
  176. ],
  177. },
  178. },
  179. ).queries[0];
  180. expect(query.orderby).toEqual([
  181. ['state', true],
  182. ['city', false],
  183. ]);
  184. });
  185. });
  186. describe('buildQuery - stable sort tie-breaker', () => {
  187. it('should add default orderby as tie-breaker for single-column CSV export', () => {
  188. const query = buildQuery(
  189. {
  190. ...basicFormData,
  191. result_format: 'csv',
  192. metrics: ['count'],
  193. },
  194. {
  195. ownState: {
  196. sortModel: [{ colId: 'state', sort: 'asc' }],
  197. },
  198. },
  199. ).queries[0];
  200. expect(query.orderby).toEqual([
  201. ['state', true],
  202. ['count', false],
  203. ]);
  204. });
  205. it('should not add tie-breaker if primary sort matches default orderby', () => {
  206. const query = buildQuery(
  207. {
  208. ...basicFormData,
  209. result_format: 'csv',
  210. metrics: ['count'],
  211. },
  212. {
  213. ownState: {
  214. sortModel: [{ colId: 'count', sort: 'desc' }],
  215. },
  216. },
  217. ).queries[0];
  218. expect(query.orderby).toEqual([['count', false]]);
  219. });
  220. it('should not add tie-breaker for multi-column sorts', () => {
  221. const query = buildQuery(
  222. {
  223. ...basicFormData,
  224. groupby: ['state', 'city'],
  225. result_format: 'csv',
  226. },
  227. {
  228. ownState: {
  229. sortModel: [
  230. { colId: 'state', sort: 'asc' },
  231. { colId: 'city', sort: 'desc' },
  232. ],
  233. },
  234. },
  235. ).queries[0];
  236. expect(query.orderby).toEqual([
  237. ['state', true],
  238. ['city', false],
  239. ]);
  240. });
  241. it('should not add tie-breaker for non-download queries with server pagination', () => {
  242. const query = buildQuery(
  243. {
  244. ...basicFormData,
  245. server_pagination: true,
  246. },
  247. {
  248. ownState: {
  249. sortBy: [{ key: 'state', desc: false }],
  250. },
  251. },
  252. ).queries[0];
  253. expect(query.orderby).toEqual([['state', true]]);
  254. });
  255. });
  256. describe('buildQuery - filter handling for CSV export', () => {
  257. it('should apply AG Grid filters for download queries', () => {
  258. const query = buildQuery(
  259. {
  260. ...basicFormData,
  261. result_format: 'csv',
  262. },
  263. {
  264. ownState: {
  265. filters: [
  266. {
  267. col: 'state',
  268. op: 'IN',
  269. val: ['CA', 'NY'],
  270. },
  271. ],
  272. },
  273. },
  274. ).queries[0];
  275. expect(query.filters).toContainEqual({
  276. col: 'state',
  277. op: 'IN',
  278. val: ['CA', 'NY'],
  279. });
  280. });
  281. it('should append AG Grid filters to existing filters', () => {
  282. const query = buildQuery(
  283. {
  284. ...basicFormData,
  285. result_format: 'csv',
  286. adhoc_filters: [
  287. {
  288. expressionType: 'SIMPLE',
  289. subject: 'country',
  290. operator: '==',
  291. comparator: 'USA',
  292. clause: 'WHERE',
  293. },
  294. ],
  295. },
  296. {
  297. ownState: {
  298. filters: [
  299. {
  300. col: 'state',
  301. op: 'IN',
  302. val: ['CA', 'NY'],
  303. },
  304. ],
  305. },
  306. },
  307. ).queries[0];
  308. expect(query.filters?.length).toBeGreaterThan(1);
  309. expect(query.filters).toContainEqual({
  310. col: 'state',
  311. op: 'IN',
  312. val: ['CA', 'NY'],
  313. });
  314. });
  315. it('should not apply filters for non-download queries', () => {
  316. const query = buildQuery(basicFormData, {
  317. ownState: {
  318. filters: [
  319. {
  320. col: 'state',
  321. op: 'IN',
  322. val: ['CA', 'NY'],
  323. },
  324. ],
  325. },
  326. }).queries[0];
  327. expect(query.filters).not.toContainEqual({
  328. col: 'state',
  329. op: 'IN',
  330. val: ['CA', 'NY'],
  331. });
  332. });
  333. it('should handle empty filters array', () => {
  334. const query = buildQuery(
  335. {
  336. ...basicFormData,
  337. result_format: 'csv',
  338. },
  339. {
  340. ownState: {
  341. filters: [],
  342. },
  343. },
  344. ).queries[0];
  345. expect(query.filters).toBeDefined();
  346. });
  347. });
  348. describe('buildQuery - column reordering for CSV export', () => {
  349. it('should reorder columns based on columnOrder', () => {
  350. const query = buildQuery(
  351. {
  352. ...basicFormData,
  353. groupby: ['state', 'city', 'country'],
  354. result_format: 'csv',
  355. },
  356. {
  357. ownState: {
  358. columnOrder: ['city', 'country', 'state', 'count'],
  359. },
  360. },
  361. ).queries[0];
  362. expect(query.columns).toEqual(['city', 'country', 'state']);
  363. });
  364. it('should reorder metrics based on columnOrder', () => {
  365. const query = buildQuery(
  366. {
  367. ...basicFormData,
  368. metrics: ['count', 'revenue', 'profit'],
  369. result_format: 'csv',
  370. },
  371. {
  372. ownState: {
  373. columnOrder: ['state', 'profit', 'count', 'revenue'],
  374. },
  375. },
  376. ).queries[0];
  377. expect(query.metrics).toEqual(['profit', 'count', 'revenue']);
  378. });
  379. it('should preserve unmatched columns at the end', () => {
  380. const query = buildQuery(
  381. {
  382. ...basicFormData,
  383. groupby: ['state', 'city', 'country'],
  384. result_format: 'csv',
  385. },
  386. {
  387. ownState: {
  388. columnOrder: ['city'],
  389. },
  390. },
  391. ).queries[0];
  392. expect(query.columns?.[0]).toEqual('city');
  393. expect(query.columns).toContain('state');
  394. expect(query.columns).toContain('country');
  395. });
  396. it('should match AdhocColumn by sqlExpression in columnOrder', () => {
  397. const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
  398. const query = buildQuery(
  399. {
  400. ...basicFormData,
  401. groupby: ['state', adhocColumn],
  402. result_format: 'csv',
  403. },
  404. {
  405. ownState: {
  406. columnOrder: ['degree_type', 'state'],
  407. },
  408. },
  409. ).queries[0];
  410. expect(query.columns?.[0]).toMatchObject({
  411. sqlExpression: 'degree_type',
  412. });
  413. });
  414. it('should not reorder for non-download queries', () => {
  415. const query = buildQuery(
  416. {
  417. ...basicFormData,
  418. groupby: ['state', 'city'],
  419. },
  420. {
  421. ownState: {
  422. columnOrder: ['city', 'state'],
  423. },
  424. },
  425. ).queries[0];
  426. expect(query.columns).toEqual(['state', 'city']);
  427. });
  428. });
  429. });