@quillsql/react 2.13.37 → 2.13.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/cjs/Chart.d.ts +2 -1
  2. package/dist/cjs/Chart.d.ts.map +1 -1
  3. package/dist/cjs/Chart.js +1 -2
  4. package/dist/cjs/ChartBuilder.d.ts.map +1 -1
  5. package/dist/cjs/ChartBuilder.js +68 -57
  6. package/dist/cjs/ReportBuilder.d.ts.map +1 -1
  7. package/dist/cjs/ReportBuilder.js +69 -51
  8. package/dist/cjs/components/Chart/ChartTooltip.d.ts +1 -1
  9. package/dist/cjs/components/Chart/ChartTooltip.d.ts.map +1 -1
  10. package/dist/cjs/components/Chart/ChartTooltip.js +3 -3
  11. package/dist/cjs/components/Chart/InternalChart.d.ts.map +1 -1
  12. package/dist/cjs/components/Chart/InternalChart.js +7 -1
  13. package/dist/cjs/components/Chart/LineChart.d.ts.map +1 -1
  14. package/dist/cjs/components/Chart/LineChart.js +4 -4
  15. package/dist/cjs/components/Dashboard/MetricComponent.d.ts.map +1 -1
  16. package/dist/cjs/components/Dashboard/MetricComponent.js +3 -3
  17. package/dist/cjs/components/ReportBuilder/convert.d.ts +1 -1
  18. package/dist/cjs/components/ReportBuilder/convert.d.ts.map +1 -1
  19. package/dist/cjs/components/ReportBuilder/convert.js +73 -21
  20. package/dist/cjs/components/UiComponents.d.ts +2 -1
  21. package/dist/cjs/components/UiComponents.d.ts.map +1 -1
  22. package/dist/cjs/components/UiComponents.js +11 -4
  23. package/dist/cjs/hooks/useExport.d.ts.map +1 -1
  24. package/dist/cjs/hooks/useExport.js +4 -5
  25. package/dist/cjs/internals/ReportBuilder/PivotForm.d.ts +19 -11
  26. package/dist/cjs/internals/ReportBuilder/PivotForm.d.ts.map +1 -1
  27. package/dist/cjs/internals/ReportBuilder/PivotForm.js +62 -48
  28. package/dist/cjs/internals/ReportBuilder/PivotList.js +5 -4
  29. package/dist/cjs/internals/ReportBuilder/PivotModal.d.ts +28 -31
  30. package/dist/cjs/internals/ReportBuilder/PivotModal.d.ts.map +1 -1
  31. package/dist/cjs/internals/ReportBuilder/PivotModal.js +315 -633
  32. package/dist/cjs/models/Pivot.d.ts +27 -7
  33. package/dist/cjs/models/Pivot.d.ts.map +1 -1
  34. package/dist/cjs/utils/astProcessing.js +2 -2
  35. package/dist/cjs/utils/dashboard.d.ts.map +1 -1
  36. package/dist/cjs/utils/dashboard.js +36 -11
  37. package/dist/cjs/utils/merge.d.ts.map +1 -1
  38. package/dist/cjs/utils/merge.js +2 -1
  39. package/dist/cjs/utils/pivotConstructor.d.ts +1 -0
  40. package/dist/cjs/utils/pivotConstructor.d.ts.map +1 -1
  41. package/dist/cjs/utils/pivotConstructor.js +37 -7
  42. package/dist/cjs/utils/pivotProcessing.d.ts.map +1 -1
  43. package/dist/cjs/utils/pivotProcessing.js +10 -14
  44. package/dist/cjs/utils/queryConstructor.d.ts.map +1 -1
  45. package/dist/cjs/utils/queryConstructor.js +421 -134
  46. package/dist/cjs/utils/report.d.ts.map +1 -1
  47. package/dist/cjs/utils/report.js +2 -2
  48. package/dist/cjs/utils/textProcessing.d.ts +1 -1
  49. package/dist/cjs/utils/textProcessing.d.ts.map +1 -1
  50. package/dist/cjs/utils/textProcessing.js +3 -0
  51. package/dist/esm/Chart.d.ts +2 -1
  52. package/dist/esm/Chart.d.ts.map +1 -1
  53. package/dist/esm/Chart.js +1 -2
  54. package/dist/esm/ChartBuilder.d.ts.map +1 -1
  55. package/dist/esm/ChartBuilder.js +68 -57
  56. package/dist/esm/ReportBuilder.d.ts.map +1 -1
  57. package/dist/esm/ReportBuilder.js +69 -51
  58. package/dist/esm/components/Chart/ChartTooltip.d.ts +1 -1
  59. package/dist/esm/components/Chart/ChartTooltip.d.ts.map +1 -1
  60. package/dist/esm/components/Chart/ChartTooltip.js +3 -3
  61. package/dist/esm/components/Chart/InternalChart.d.ts.map +1 -1
  62. package/dist/esm/components/Chart/InternalChart.js +7 -1
  63. package/dist/esm/components/Chart/LineChart.d.ts.map +1 -1
  64. package/dist/esm/components/Chart/LineChart.js +4 -4
  65. package/dist/esm/components/Dashboard/MetricComponent.d.ts.map +1 -1
  66. package/dist/esm/components/Dashboard/MetricComponent.js +3 -3
  67. package/dist/esm/components/ReportBuilder/convert.d.ts +1 -1
  68. package/dist/esm/components/ReportBuilder/convert.d.ts.map +1 -1
  69. package/dist/esm/components/ReportBuilder/convert.js +74 -22
  70. package/dist/esm/components/UiComponents.d.ts +2 -1
  71. package/dist/esm/components/UiComponents.d.ts.map +1 -1
  72. package/dist/esm/components/UiComponents.js +11 -4
  73. package/dist/esm/hooks/useExport.d.ts.map +1 -1
  74. package/dist/esm/hooks/useExport.js +4 -5
  75. package/dist/esm/internals/ReportBuilder/PivotForm.d.ts +19 -11
  76. package/dist/esm/internals/ReportBuilder/PivotForm.d.ts.map +1 -1
  77. package/dist/esm/internals/ReportBuilder/PivotForm.js +63 -49
  78. package/dist/esm/internals/ReportBuilder/PivotList.js +5 -4
  79. package/dist/esm/internals/ReportBuilder/PivotModal.d.ts +28 -31
  80. package/dist/esm/internals/ReportBuilder/PivotModal.d.ts.map +1 -1
  81. package/dist/esm/internals/ReportBuilder/PivotModal.js +327 -635
  82. package/dist/esm/models/Pivot.d.ts +27 -7
  83. package/dist/esm/models/Pivot.d.ts.map +1 -1
  84. package/dist/esm/utils/astProcessing.js +2 -2
  85. package/dist/esm/utils/dashboard.d.ts.map +1 -1
  86. package/dist/esm/utils/dashboard.js +36 -11
  87. package/dist/esm/utils/merge.d.ts.map +1 -1
  88. package/dist/esm/utils/merge.js +2 -1
  89. package/dist/esm/utils/pivotConstructor.d.ts +1 -0
  90. package/dist/esm/utils/pivotConstructor.d.ts.map +1 -1
  91. package/dist/esm/utils/pivotConstructor.js +37 -8
  92. package/dist/esm/utils/pivotProcessing.d.ts.map +1 -1
  93. package/dist/esm/utils/pivotProcessing.js +10 -14
  94. package/dist/esm/utils/queryConstructor.d.ts.map +1 -1
  95. package/dist/esm/utils/queryConstructor.js +421 -134
  96. package/dist/esm/utils/report.d.ts.map +1 -1
  97. package/dist/esm/utils/report.js +2 -2
  98. package/dist/esm/utils/textProcessing.d.ts +1 -1
  99. package/dist/esm/utils/textProcessing.d.ts.map +1 -1
  100. package/dist/esm/utils/textProcessing.js +3 -0
  101. package/package.json +1 -1
@@ -212,7 +212,7 @@ function generateDistinctQueryMySQL(stringFields, query) {
212
212
  }
213
213
  function generateDistinctQueryPostgres(stringFields, query) {
214
214
  const distinctQueries = stringFields.map((field) => {
215
- return `SELECT '${field}' AS ${processColumnReference('field', 'postgresql')}, to_json(ARRAY_AGG(DISTINCT ${field})) AS ${processColumnReference('string_values', 'postgresql')} FROM querytable`;
215
+ return `SELECT '${field}' AS ${processColumnReference('field', 'postgresql')}, to_json(ARRAY_AGG(DISTINCT "${field}")) AS ${processColumnReference('string_values', 'postgresql')} FROM querytable`;
216
216
  });
217
217
  const distinctQuery = distinctQueries.join(' UNION ALL ');
218
218
  return `WITH querytable AS (${query.replace(';', '')}) ` + distinctQuery;
@@ -283,53 +283,115 @@ function create2DPivotQuery(pivot, itemQuery, databaseType, columnFieldValues, d
283
283
  if (!pivot || !pivot.columnField) {
284
284
  return undefined;
285
285
  }
286
- if (isStringType(pivot.rowFieldType || '') || !pivot.rowFieldType) {
286
+ if (isStringType(pivot.rowFieldType ?? '') || !pivot.rowFieldType) {
287
287
  return create2DStringPivotQuery(pivot, itemQuery, columnFieldValues, databaseType);
288
288
  }
289
289
  return create2DDatePivotQuery(pivot, itemQuery, columnFieldValues, databaseType, dateBucket);
290
290
  }
291
291
  function create2DStringPivotQuery(pivot, itemQuery, columnFieldValues, databaseType) {
292
292
  const isValidBaseQuery = itemQuery.match(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/);
293
- if (!isValidBaseQuery || !pivot.columnField || !pivot.rowField) {
293
+ if (!isValidBaseQuery || !pivot.columnField || !pivot.rowField)
294
294
  return undefined;
295
- }
296
295
  const rowField = pivot.rowField;
297
- const valueField = pivot.valueField;
296
+ if (!pivot.aggregations?.[0]?.valueField && !pivot.valueField)
297
+ throw new Error('No value field provided for pivot');
298
+ if (!pivot.aggregations?.[0]?.aggregationType && !pivot.aggregationType)
299
+ throw new Error('No aggregation type provided for pivot');
298
300
  const columnField = pivot.columnField;
299
301
  const rowFieldAlias = processColumnReference(rowField, databaseType, undefined, false, true);
300
- const valueFieldAlias = processColumnReference(valueField ?? rowField, databaseType, undefined, false, true);
301
302
  const columnFieldAlias = processColumnReference(columnField, databaseType, undefined, false, true);
302
- const valueAliasSubstring = valueField
303
- ? `${processColumnReference(valueField, databaseType, undefined, true)} AS ${valueFieldAlias},`
304
- : '';
305
- let value2AliasSubstring = '';
306
- let caseWhens = columnFieldValues.map((column) => {
307
- return `${processAggType(pivot.aggregationType, true)}(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) AS ${processColumnReference(column, databaseType, '_', true)}`;
308
- });
309
- if (pivot.aggregationType === 'percentage' && pivot.valueField2) {
310
- const valueField2Alias = processColumnReference(pivot.valueField2, databaseType, undefined, false, true);
311
- if (pivot.valueField === pivot.valueField2) {
312
- caseWhens = columnFieldValues.map((column) => {
313
- return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) / GREATEST(sum(${processColumnReference(pivot.valueField2, databaseType)}), 1) AS ${processColumnReference(column, databaseType, '_', true)}`;
314
- });
303
+ let caseWhens = [];
304
+ let valueAliases = [];
305
+ const seenAggs = {};
306
+ pivot.aggregations?.forEach((currentAgg) => {
307
+ // Track duplicate aggregation combos for disambiguation.
308
+ if (seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']) {
309
+ seenAggs[currentAgg.aggregationType ?? ''][currentAgg.valueField ?? ''] += 1;
315
310
  }
316
311
  else {
317
- value2AliasSubstring = `${processColumnReference(pivot.valueField2, databaseType, undefined, true)} AS ${valueField2Alias},`;
318
- caseWhens = columnFieldValues.map((column) => {
319
- return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) / GREATEST(sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueField2Alias)} END), 1) AS ${processColumnReference(column, databaseType, '_', true)}`;
320
- });
312
+ seenAggs[currentAgg.aggregationType ?? ''] = {
313
+ ...seenAggs[currentAgg.aggregationType ?? ''],
314
+ [currentAgg.valueField ?? '']: 1,
315
+ };
321
316
  }
322
- }
323
- // pivot sort matters in the base query when there is a rowLimit. In mssql, an orderby must be accompanied by a limit in a subquery and not allowed in a cte
324
- const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} ` : ''}`;
317
+ let disambiguationIndex = seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']?.toString();
318
+ if (disambiguationIndex === '1')
319
+ disambiguationIndex = '';
320
+ const valueFieldAlias = processColumnReference((currentAgg.valueField || rowField || 'count'), databaseType, undefined, false, true);
321
+ const valueAliasSubstring = currentAgg.valueField
322
+ ? `${processColumnReference(currentAgg.valueField, databaseType, undefined, true)} AS ${valueFieldAlias}`
323
+ : '';
324
+ let value2AliasSubstring = '';
325
+ const disambiguationField = Object.values(seenAggs[currentAgg.aggregationType ?? ''] ?? {}).reduce((acc, v) => acc + v) > 1
326
+ ? `_${currentAgg.valueField}${disambiguationIndex}`
327
+ : '';
328
+ const disambiguation = pivot.aggregations?.length > 1
329
+ ? `${disambiguationField}_${disambiguationField ? matchCasing(currentAgg.aggregationType, currentAgg.valueField) : currentAgg.aggregationType}`
330
+ : '';
331
+ // Wrap the value field in CASE WHEN if its type is bool.
332
+ const valueExpr = currentAgg.valueFieldType === 'bool'
333
+ ? `CASE WHEN ${valueFieldAlias} THEN 1 ELSE 0 END`
334
+ : processValueField(currentAgg.aggregationType, databaseType, valueFieldAlias);
335
+ if (currentAgg.aggregationType === 'percentage') {
336
+ if (!currentAgg.valueField)
337
+ throw new Error('No value field provided for pivot');
338
+ const valueField2Alias = processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, false, true);
339
+ // Wrap the second value field if its type is bool.
340
+ const value2Expr = (currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool'
341
+ ? `CASE WHEN ${valueField2Alias} THEN 1 ELSE 0 END`
342
+ : valueField2Alias;
343
+ if (currentAgg.valueField === currentAgg.valueField2 || !currentAgg.valueField2) {
344
+ caseWhens = columnFieldValues.map((column) => {
345
+ return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) / GREATEST(sum(${value2Expr}), 1) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
346
+ });
347
+ }
348
+ else {
349
+ value2AliasSubstring = `${processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, true)} AS ${valueField2Alias}`;
350
+ caseWhens = columnFieldValues.map((column) => {
351
+ return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) / GREATEST(sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${value2Expr} END), 1) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
352
+ });
353
+ }
354
+ }
355
+ else {
356
+ caseWhens = [
357
+ ...caseWhens,
358
+ ...columnFieldValues.map((column) => {
359
+ return `${processAggType(currentAgg.aggregationType, true)}(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
360
+ }),
361
+ ];
362
+ }
363
+ if (valueAliasSubstring)
364
+ valueAliases.push(valueAliasSubstring);
365
+ if (value2AliasSubstring)
366
+ valueAliases.push(value2AliasSubstring);
367
+ });
368
+ valueAliases = [
369
+ `${processColumnReference(rowField, databaseType, undefined, true)} AS ${rowFieldAlias}`,
370
+ ...valueAliases,
371
+ `${processColumnReference(columnField, databaseType, undefined, true)} AS ${columnFieldAlias}`,
372
+ ];
373
+ valueAliases = Array.from(new Set(valueAliases));
374
+ const sortQuery = pivot.sort && pivot.sortField && pivot.rowLimit
375
+ ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} `
376
+ : '';
325
377
  const pivotQuery = `
326
- ,quill_alias AS (SELECT ${processColumnReference(rowField, databaseType, undefined, true)} AS ${rowFieldAlias}, ${valueAliasSubstring} ${value2AliasSubstring}
327
- ${processColumnReference(columnField, databaseType, undefined, true)} AS ${columnFieldAlias} FROM quill_base_table),
328
- quill_qt_cw AS (SELECT ${rowFieldAlias}${caseWhens.length > 0 ? `, ${caseWhens.join(', ')}` : ''} FROM quill_alias GROUP BY ${rowFieldAlias}),
329
- quill_base_pivot AS (SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} * FROM quill_qt_cw qt
330
- ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''})
378
+ ,quill_alias AS (
379
+ SELECT ${valueAliases.length > 0 ? `${valueAliases.join(', ')}` : ''}
380
+ FROM quill_base_table
381
+ ),
382
+ quill_qt_cw AS (
383
+ SELECT ${rowFieldAlias}
384
+ ${caseWhens.length > 0 ? `, ${caseWhens.join(', ')}` : ''}
385
+ FROM quill_alias
386
+ GROUP BY ${rowFieldAlias}
387
+ ),
388
+ quill_base_pivot AS (
389
+ SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} *
390
+ FROM quill_qt_cw qt
391
+ ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''}
392
+ )
331
393
  SELECT * FROM quill_base_pivot
332
- `
394
+ `
333
395
  .replace(/\s+/g, ' ')
334
396
  .trim();
335
397
  return itemQuery.replace(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/, pivotQuery);
@@ -339,38 +401,95 @@ function create2DDatePivotQuery(pivot, itemQuery, columnFieldValues, databaseTyp
339
401
  if (!isValidBaseQuery || !pivot.columnField || !pivot.rowField) {
340
402
  return undefined;
341
403
  }
404
+ if (!pivot.aggregations?.[0]?.valueField && !pivot.valueField)
405
+ throw new Error('No value field provided for pivot');
406
+ if (!pivot.aggregations?.[0]?.aggregationType && !pivot.aggregationType)
407
+ throw new Error('No aggregation type provided for pivot');
408
+ // const aggregationType = (pivot.aggregations?.[0]?.aggregationType ?? pivot.aggregationType) as any;
342
409
  const rowField = pivot.rowField;
343
410
  const columnField = pivot.columnField;
344
411
  const rowFieldAlias = processColumnReference(rowField, databaseType, undefined, false, true);
345
- const valueFieldAlias = processColumnReference(pivot.valueField ?? rowField, databaseType, undefined, false, true);
346
412
  const columnFieldAlias = processColumnReference(columnField, databaseType, undefined, false, true);
347
- const valueAliasSubstring = pivot.valueField
348
- ? `${processColumnReference(pivot.valueField, databaseType, undefined, true)} AS ${valueFieldAlias},`
349
- : '';
350
- let value2AliasSubstring = '';
351
- let caseWhens = columnFieldValues.map((column) => {
352
- return `${processAggType(pivot.aggregationType, true)}(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) AS ${processColumnReference(column, databaseType, '_', true)}`;
353
- });
354
- if (pivot.aggregationType === 'percentage' && pivot.valueField2) {
355
- const valueField2Alias = processColumnReference(pivot.valueField2, databaseType, undefined, false, true);
356
- // edge case. if the user picks amount and amount, we assume they want a pie chart like breakdown of amount. so the summation of valueField2 has to be moved outside of the case when
357
- if (pivot.valueField === pivot.valueField2) {
358
- caseWhens = columnFieldValues.map((column) => {
359
- return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) / GREATEST(sum(${processColumnReference(valueField2Alias, databaseType)}), 1) AS ${processColumnReference(column, databaseType, '_', true)}`;
360
- });
413
+ let caseWhens = [];
414
+ let valueFieldAliases = [];
415
+ const seenAggs = {};
416
+ pivot.aggregations?.forEach((currentAgg) => {
417
+ // if the aggregation combo has been seen before, increment the count, else add it to the seenAggs array
418
+ if (seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']) {
419
+ seenAggs[currentAgg.aggregationType ?? ''][currentAgg.valueField ?? ''] += 1;
361
420
  }
362
421
  else {
363
- value2AliasSubstring = `${processColumnReference(pivot.valueField2, databaseType, undefined, true)} AS ${valueField2Alias},`;
364
- caseWhens = columnFieldValues.map((column) => {
365
- return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueFieldAlias)} END) / GREATEST(sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${processValueField(pivot.aggregationType, databaseType, valueField2Alias)} END), 1) AS ${processColumnReference(column, databaseType, '_', true)}`;
366
- });
422
+ seenAggs[currentAgg.aggregationType ?? ''] = {
423
+ ...seenAggs[currentAgg.aggregationType ?? ''],
424
+ [currentAgg.valueField ?? '']: 1,
425
+ };
367
426
  }
368
- }
427
+ let disambiguationIndex = seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']?.toString();
428
+ if (disambiguationIndex === '1')
429
+ disambiguationIndex = '';
430
+ const valueFieldAlias = processColumnReference((currentAgg.valueField ?? rowField), databaseType, undefined, false, true);
431
+ const valueAliasSubstring = currentAgg.valueField
432
+ ? `${processColumnReference(currentAgg.valueField, databaseType, undefined, true)} AS ${valueFieldAlias}`
433
+ : '';
434
+ let value2AliasSubstring = '';
435
+ const disambiguationField = Object.values(seenAggs[currentAgg.aggregationType ?? ''] ?? {}).reduce((acc, v) => acc + v) > 1 ? `_${currentAgg.valueField}${disambiguationIndex}` : '';
436
+ const disambiguation = pivot.aggregations?.length > 1
437
+ ? `${disambiguationField}_${disambiguationField ? matchCasing(currentAgg.aggregationType, currentAgg.valueField) : currentAgg.aggregationType}`
438
+ : '';
439
+ // Wrap the value field in CASE WHEN if its type is bool.
440
+ const valueExpr = currentAgg.valueFieldType === 'bool'
441
+ ? `CASE WHEN ${valueFieldAlias} THEN 1 ELSE 0 END`
442
+ : processValueField(currentAgg.aggregationType, databaseType, valueFieldAlias);
443
+ if (currentAgg.aggregationType === 'percentage') {
444
+ if (!currentAgg.valueField)
445
+ throw new Error('No value field provided for pivot');
446
+ const valueField2Alias = processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, false, true);
447
+ // Wrap the second value field if its type is bool.
448
+ const value2Expr = (currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool'
449
+ ? `CASE WHEN ${valueField2Alias} THEN 1 ELSE 0 END`
450
+ : valueField2Alias;
451
+ // edge case. if the user picks amount and amount, we assume they want a pie chart like breakdown of amount. so the summation of valueField2 has to be moved outside of the case whe
452
+ if (currentAgg.valueField === currentAgg.valueField2 || !currentAgg.valueField2) {
453
+ caseWhens = [
454
+ ...caseWhens,
455
+ ...columnFieldValues.map((column) => {
456
+ return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) / GREATEST(sum(${value2Expr}), 1) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
457
+ }),
458
+ ];
459
+ }
460
+ else {
461
+ value2AliasSubstring = `${processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, true)} AS ${valueField2Alias}`;
462
+ caseWhens = [
463
+ ...caseWhens,
464
+ ...columnFieldValues.map((column) => {
465
+ return `sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) / GREATEST(sum(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${value2Expr} END), 1) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
466
+ }),
467
+ ];
468
+ }
469
+ }
470
+ else {
471
+ caseWhens = [
472
+ ...caseWhens,
473
+ ...columnFieldValues.map((column) => {
474
+ return `${processAggType(currentAgg.aggregationType, true)}(CASE WHEN ${columnFieldAlias} = '${processSingleQuotes(column, databaseType)}' THEN ${valueExpr} END) AS ${processColumnReference(column + disambiguation, databaseType, '_', true)}`;
475
+ }),
476
+ ];
477
+ }
478
+ if (valueAliasSubstring)
479
+ valueFieldAliases.push(valueAliasSubstring);
480
+ if (value2AliasSubstring)
481
+ valueFieldAliases.push(value2AliasSubstring);
482
+ });
483
+ valueFieldAliases = [
484
+ `${processColumnReference(rowField, databaseType, undefined, true)} AS ${rowFieldAlias}`,
485
+ ...valueFieldAliases,
486
+ `${processColumnReference(columnField, databaseType, undefined, true)} AS ${columnFieldAlias}`,
487
+ ];
488
+ valueFieldAliases = Array.from(new Set(valueFieldAliases));
369
489
  // pivot sort matters in the base query when there is a rowLimit. In mssql, an orderby must be accompanied by a limit in a subquery and not allowed in a cte
370
490
  const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} ` : ''}`;
371
491
  const pivotQuery = `
372
- , quill_alias AS (SELECT ${processColumnReference(`${rowField}`, databaseType, undefined, true)} AS ${rowFieldAlias}, ${valueAliasSubstring} ${value2AliasSubstring}
373
- ${processColumnReference(columnField, databaseType, undefined, true, true)} AS ${columnFieldAlias} FROM quill_base_table),
492
+ , quill_alias AS (SELECT ${valueFieldAliases.length > 0 ? `${valueFieldAliases.join(', ')}` : ''} FROM quill_base_table),
374
493
  quill_qt_agg AS (SELECT ${processDateTrunc(dateBucket, rowFieldAlias, databaseType)} as ${rowFieldAlias}${caseWhens.length > 0 ? `, ${caseWhens.join(', ')}` : ''} FROM quill_alias GROUP BY ${databaseType.toLowerCase() === 'clickhouse' ? processColumnReference(`${rowField}`, databaseType) : processDateTrunc(dateBucket, rowFieldAlias, databaseType)}),
375
494
  quill_base_pivot AS (SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} * FROM quill_qt_agg qt
376
495
  ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''})
@@ -391,37 +510,91 @@ function create1DPivotQuery(pivot, itemQuery, dateBucket = 'month', databaseType
391
510
  // their own with statements and withs with the same name will conflict
392
511
  function create1DStringPivotQuery(pivot, itemQuery, databaseType) {
393
512
  const isValidBaseQuery = itemQuery.match(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/);
394
- if (!isValidBaseQuery) {
513
+ if (!isValidBaseQuery)
395
514
  return undefined;
396
- }
397
515
  const rowField = pivot.rowField;
398
- const valueField = pivot.valueField;
399
- const rowAlias = processColumnReference(rowField, databaseType, undefined, false, true);
400
- const valueAlias = processColumnReference(valueField ?? rowField, databaseType, undefined, false, true);
401
- const countAlias = processColumnReference('count', databaseType);
402
- const valueAliasSubstring = valueField
403
- ? `, ${processColumnReference(`${valueField}`, databaseType, undefined, true)} AS ${valueAlias}`
404
- : ``;
405
- let value2AliasSubstring = '';
406
- let quillSelectSubstring = `${rowAlias}, ${processAggType(pivot.aggregationType)}(${valueField ? valueAlias : '*'}) as ${valueField ? valueAlias : countAlias}`;
407
- if (pivot.aggregationType === 'percentage' && pivot.valueField2) {
408
- const value2Alias = processColumnReference(pivot.valueField2, databaseType, undefined, false, true);
409
- if (pivot.valueField !== pivot.valueField2) {
410
- value2AliasSubstring =
411
- valueField && valueField !== pivot.valueField2
412
- ? `, ${processColumnReference(`${pivot.valueField2}`, databaseType, undefined, true)} AS ${value2Alias}`
413
- : '';
516
+ const rowAlias = processColumnReference(rowField, databaseType, undefined, true);
517
+ let quillAggSelects = [rowAlias];
518
+ let valueFieldAliases = [];
519
+ const seenAggs = {};
520
+ pivot.aggregations?.forEach((currentAgg) => {
521
+ if (!currentAgg.valueField)
522
+ currentAgg.valueField = undefined;
523
+ // Track duplicates to disambiguate names
524
+ if (seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']) {
525
+ seenAggs[currentAgg.aggregationType ?? ''][currentAgg.valueField ?? ''] += 1;
414
526
  }
415
- quillSelectSubstring = `${rowAlias}, sum(${valueAlias}) / GREATEST(sum(${value2Alias}), 1) as ${processColumnReference(`${valueField ?? ''}_${matchCasing('percentage', valueField)}`, databaseType, undefined, false, true)}`;
416
- }
417
- // pivot sort matters in the base query when there is a rowLimit. In mssql, an orderby must be accompanied by a limit in a subquery and not allowed in a cte
418
- const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} ` : ''}`;
419
- const pivotQuery = `, quill_alias AS (SELECT ${processColumnReference(`${rowField}`, databaseType, undefined, true)} AS ${rowAlias}${valueAliasSubstring}${value2AliasSubstring} FROM quill_base_table),
420
- quill_qt_cw AS (SELECT ${quillSelectSubstring} FROM quill_alias GROUP BY ${rowAlias}),
421
- quill_base_pivot AS (SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} * FROM quill_qt_cw qt
422
- ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''})
423
- SELECT * FROM quill_base_pivot
424
- `
527
+ else {
528
+ seenAggs[currentAgg.aggregationType ?? ''] = {
529
+ ...seenAggs[currentAgg.aggregationType ?? ''],
530
+ [currentAgg.valueField ?? '']: 1,
531
+ };
532
+ }
533
+ let disambiguationIndex = seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']?.toString() ?? '';
534
+ if (disambiguationIndex === '1')
535
+ disambiguationIndex = '';
536
+ // This is the alias (from quill_alias CTE) for the field
537
+ const valueFieldAlias = processColumnReference((currentAgg.valueField || rowField || 'count'), databaseType, undefined, false, true);
538
+ // In the base query, we select the raw column.
539
+ const valueAliasSubstring = currentAgg.valueField
540
+ ? `${processColumnReference(currentAgg.valueField, databaseType, undefined, true)} AS ${valueFieldAlias}`
541
+ : '';
542
+ let value2AliasSubstring = '';
543
+ const disambiguation = pivot.aggregations?.length > 1
544
+ ? `${disambiguationIndex}_${currentAgg.aggregationType}`
545
+ : '';
546
+ // Wrap the field in a CASE WHEN if it's boolean
547
+ let valueExpr = !currentAgg.valueField ? '*' : valueFieldAlias;
548
+ if (currentAgg.valueFieldType === 'bool') {
549
+ valueExpr = `CASE WHEN ${valueFieldAlias} THEN 1 ELSE 0 END`;
550
+ }
551
+ if (currentAgg.aggregationType === 'percentage') {
552
+ if (!currentAgg.valueField) {
553
+ throw new Error('No value field provided for percentage aggregation');
554
+ }
555
+ const valueField2Alias = processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, false, true);
556
+ let value2Expr = valueField2Alias;
557
+ if ((currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool') {
558
+ value2Expr = `CASE WHEN ${valueField2Alias} THEN 1 ELSE 0 END`;
559
+ }
560
+ value2AliasSubstring = currentAgg.valueField2 && currentAgg.valueField !== currentAgg.valueField2
561
+ ? `${processColumnReference(currentAgg.valueField2, databaseType, undefined, true)} AS ${valueField2Alias}`
562
+ : '';
563
+ const percentageExpr = currentAgg.valueField === currentAgg.valueField2 || !currentAgg.valueField2
564
+ ? `sum(${valueExpr}) / ${(currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool' ? 'COUNT(*)' : 'SUM(sum(' + valueExpr + ')) OVER ()'}`
565
+ : `sum(${valueExpr}) / GREATEST(sum(${value2Expr}), 1)`;
566
+ quillAggSelects = [
567
+ ...quillAggSelects,
568
+ `${percentageExpr} as ${processColumnReference(`${currentAgg.valueField ?? ''}${disambiguation}`, databaseType, undefined, false, true)}`,
569
+ ];
570
+ }
571
+ else {
572
+ quillAggSelects = [
573
+ ...quillAggSelects,
574
+ `${processAggType(currentAgg.aggregationType)}(${valueExpr}) AS ${processColumnReference((currentAgg.valueField || 'count') + disambiguation, databaseType)}`,
575
+ ];
576
+ }
577
+ if (valueAliasSubstring)
578
+ valueFieldAliases.push(valueAliasSubstring);
579
+ if (value2AliasSubstring)
580
+ valueFieldAliases.push(value2AliasSubstring);
581
+ });
582
+ valueFieldAliases = Array.from(new Set(valueFieldAliases));
583
+ const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit
584
+ ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} `
585
+ : ''}`;
586
+ const pivotQuery = `, quill_alias AS (
587
+ SELECT ${processColumnReference(`${rowField}`, databaseType, undefined, true)} AS ${rowAlias}${valueFieldAliases.length > 0 ? `, ${valueFieldAliases.join(', ')}` : ''}
588
+ FROM quill_base_table
589
+ ),
590
+ quill_qt_cw AS (
591
+ SELECT ${quillAggSelects.join(', ')} FROM quill_alias GROUP BY ${rowAlias}
592
+ ),
593
+ quill_base_pivot AS (
594
+ SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} *
595
+ FROM quill_qt_cw qt ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''}
596
+ )
597
+ SELECT * FROM quill_base_pivot`
425
598
  .replace(/\s+/g, ' ')
426
599
  .trim();
427
600
  return itemQuery.replace(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/, pivotQuery);
@@ -432,33 +605,76 @@ function create1DDatePivotQuery(pivot, itemQuery, dateBucket = 'month', database
432
605
  return undefined;
433
606
  }
434
607
  const rowField = pivot.rowField || '';
435
- const valueField = pivot.valueField;
436
608
  const rowFieldAlias = processColumnReference(rowField, databaseType, undefined);
437
- const valueFieldAlias = processColumnReference(valueField ?? rowField, databaseType, undefined, false, true);
438
- const comparisonValueFieldAlias = processColumnReference(valueField ?? rowField, databaseType, undefined, false, true);
439
- const countAlias = processColumnReference(`count`, databaseType);
440
- const valueAliasSubstring = valueField ? `, ${valueFieldAlias}` : ``;
441
- let value2AliasSubstring = '';
442
- let quillAggSubstring = `
443
- ${processDateTrunc(dateBucket, rowFieldAlias, databaseType)} as ${processColumnReference(`${rowField}`, databaseType)}
444
- , ${processAggType(pivot.aggregationType)}(${valueField ? valueFieldAlias : rowFieldAlias}) as ${valueField ? comparisonValueFieldAlias : countAlias}
445
- `;
446
- // this "and" is for typescript. in reality, pivot.valueField2 must exist if pivot.aggregationType === 'percentage'
447
- if (pivot.aggregationType === 'percentage' && pivot.valueField2) {
448
- const valueField2Alias = processColumnReference(pivot.valueField2, databaseType, undefined, false, true);
449
- value2AliasSubstring =
450
- valueField !== pivot.valueField2 ? `, ${valueField2Alias}` : '';
451
- quillAggSubstring = `
452
- ${processDateTrunc(dateBucket, rowFieldAlias, databaseType)} as ${processColumnReference(rowField, databaseType)}
453
- , sum(${valueFieldAlias}) / GREATEST(sum(${valueField2Alias}), 1) as ${processColumnReference(`${valueField ?? ''}_${matchCasing('percentage', valueField)}`, databaseType, undefined, false, true)}
454
- `;
455
- }
609
+ let quillAggSelects = [`${processDateTrunc(dateBucket, rowFieldAlias, databaseType)} as ${processColumnReference(rowField, databaseType)}`];
610
+ let valueFieldAliases = [];
611
+ const seenAggs = {};
612
+ pivot.aggregations?.forEach((currentAgg) => {
613
+ if (!currentAgg.valueField)
614
+ currentAgg.valueField = undefined;
615
+ // if the aggregation combo has been seen before, increment the count, else add it to the seenAggs array
616
+ if (seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']) {
617
+ seenAggs[currentAgg.aggregationType ?? ''][currentAgg.valueField ?? ''] += 1;
618
+ }
619
+ else {
620
+ seenAggs[currentAgg.aggregationType ?? ''] = {
621
+ ...seenAggs[currentAgg.aggregationType ?? ''],
622
+ [currentAgg.valueField ?? '']: 1,
623
+ };
624
+ }
625
+ let disambiguationIndex = seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']?.toString() ?? '';
626
+ if (disambiguationIndex === '1')
627
+ disambiguationIndex = '';
628
+ const valueFieldAlias = processColumnReference((currentAgg.valueField || rowField || 'count'), databaseType, undefined, false, true);
629
+ const valueAliasSubstring = currentAgg.valueField
630
+ ? `${processColumnReference(currentAgg.valueField, databaseType, undefined, true)} AS ${valueFieldAlias}`
631
+ : '';
632
+ let value2AliasSubstring = '';
633
+ const disambiguation = pivot.aggregations?.length > 1
634
+ ? `${disambiguationIndex}_${currentAgg.aggregationType}`
635
+ : '';
636
+ // Wrap the field in a CASE WHEN if it's boolean
637
+ let valueExpr = !currentAgg.valueField ? '*' : valueFieldAlias;
638
+ if (currentAgg.valueFieldType === 'bool') {
639
+ valueExpr = `CASE WHEN ${valueFieldAlias} THEN 1 ELSE 0 END`;
640
+ }
641
+ if (currentAgg.aggregationType === 'percentage') {
642
+ if (!currentAgg.valueField) {
643
+ throw new Error('No value field provided for percentage aggregation');
644
+ }
645
+ const valueField2Alias = processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, false, true);
646
+ value2AliasSubstring = currentAgg.valueField2 && currentAgg.valueField !== currentAgg.valueField2
647
+ ? `${processColumnReference(currentAgg.valueField2, databaseType, undefined, true)} AS ${valueField2Alias}`
648
+ : '';
649
+ let value2Expr = valueField2Alias;
650
+ if ((currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool') {
651
+ value2Expr = `CASE WHEN ${valueField2Alias} THEN 1 ELSE 0 END`;
652
+ }
653
+ const percentageExpr = currentAgg.valueField === currentAgg.valueField2 || !currentAgg.valueField2
654
+ ? `sum(${valueExpr}) / ${(currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool' ? 'COUNT(*)' : 'SUM(sum(' + valueExpr + ')) OVER ()'}`
655
+ : `sum(${valueExpr}) / GREATEST(sum(${value2Expr}), 1)`;
656
+ quillAggSelects = [
657
+ ...quillAggSelects,
658
+ `${percentageExpr} as ${processColumnReference(`${currentAgg.valueField}${disambiguation}`, databaseType, undefined, false, true)}`,
659
+ ];
660
+ }
661
+ else {
662
+ quillAggSelects = [
663
+ ...quillAggSelects,
664
+ `${processAggType(currentAgg.aggregationType)}(${valueExpr}) AS ${processColumnReference((currentAgg.valueField || 'count') + disambiguation, databaseType)}`,
665
+ ];
666
+ }
667
+ if (valueAliasSubstring)
668
+ valueFieldAliases.push(valueAliasSubstring);
669
+ if (value2AliasSubstring)
670
+ valueFieldAliases.push(value2AliasSubstring);
671
+ });
672
+ valueFieldAliases = Array.from(new Set(valueFieldAliases));
456
673
  // pivot sort matters in the base query when there is a rowLimit. In mssql, an orderby must be accompanied by a limit in a subquery and not allowed in a cte
457
674
  const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} ` : ''}`;
458
- const pivotQuery = `, quill_alias AS (SELECT ${processColumnReference(`${rowField}`, databaseType, undefined, true)} AS ${rowFieldAlias}${valueAliasSubstring}
459
- ${value2AliasSubstring}
675
+ const pivotQuery = `, quill_alias AS (SELECT ${processColumnReference(`${rowField}`, databaseType, undefined, true)} AS ${rowFieldAlias}${valueFieldAliases.length > 0 ? `, ${valueFieldAliases.join(', ')}` : ''}
460
676
  FROM quill_base_table),
461
- quill_qt_agg AS (SELECT ${quillAggSubstring}
677
+ quill_qt_agg AS (SELECT ${quillAggSelects.join(', ')}
462
678
  FROM quill_alias GROUP BY ${databaseType.toLowerCase() === 'clickhouse' ? processColumnReference(`${rowField}`, databaseType) : processDateTrunc(dateBucket, rowFieldAlias, databaseType)}),
463
679
  quill_base_pivot AS (SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} * FROM quill_qt_agg qt
464
680
  ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''})
@@ -470,27 +686,89 @@ function create1DDatePivotQuery(pivot, itemQuery, dateBucket = 'month', database
470
686
  }
471
687
  function createAggregationValuePivot(pivot, itemQuery, databaseType) {
472
688
  const isValidBaseQuery = itemQuery.match(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/);
473
- if (!isValidBaseQuery || !pivot.valueField) {
689
+ if (!isValidBaseQuery)
474
690
  return undefined;
475
- }
476
- const valueField = pivot.valueField;
477
- const aggregationType = processAggType(pivot.aggregationType);
478
- const valueAlias = processColumnReference(valueField, databaseType, undefined, false, true);
479
- let value2AliasSubstring = '';
480
- let quillAggSubstring = `SELECT ${aggregationType}(${valueAlias}) as ${processColumnReference(valueField, databaseType)}`;
481
- if (aggregationType === 'percentage' && pivot.valueField2) {
482
- const value2Alias = processColumnReference(pivot.valueField2, databaseType, undefined, false, true);
483
- value2AliasSubstring = `, ${value2Alias}`;
484
- quillAggSubstring = `SELECT sum(${valueAlias}) / GREATEST(sum(${value2Alias}), 1) as ${processColumnReference(`${valueField}_${matchCasing('percentage', valueField)}`, databaseType, undefined, false, true)}`;
485
- }
486
- // pivot sort matters in the base query when there is a rowLimit. In mssql, an orderby must be accompanied by a limit in a subquery and not allowed in a cte
487
- const sortQuery = `${pivot.sort && pivot.sortField && pivot.rowLimit ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} ` : ''}`;
488
- const pivotQuery = `, quill_alias AS (SELECT ${valueAlias}${value2AliasSubstring} FROM quill_base_table),
489
- quill_qt_agg AS (${quillAggSubstring} FROM quill_alias),
490
- quill_base_pivot AS (SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql' ? `TOP ${pivot.rowLimit}` : ''} * FROM quill_qt_agg qt
491
- ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql' ? ` LIMIT ${pivot.rowLimit}` : ''})
492
- SELECT * FROM quill_base_pivot
493
- `
691
+ let quillAggSelects = [];
692
+ let valueFieldAliases = [];
693
+ const seenAggs = {};
694
+ pivot.aggregations?.forEach((currentAgg) => {
695
+ if (!currentAgg.valueField)
696
+ currentAgg.valueField = undefined;
697
+ // Track duplicate aggregation combos for disambiguation.
698
+ if (seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']) {
699
+ seenAggs[currentAgg.aggregationType ?? ''][currentAgg.valueField ?? ''] += 1;
700
+ }
701
+ else {
702
+ seenAggs[currentAgg.aggregationType ?? ''] = {
703
+ ...seenAggs[currentAgg.aggregationType ?? ''],
704
+ [currentAgg.valueField ?? '']: 1,
705
+ };
706
+ }
707
+ let disambiguationIndex = seenAggs[currentAgg.aggregationType ?? '']?.[currentAgg.valueField ?? '']?.toString() ?? '';
708
+ if (disambiguationIndex === '1')
709
+ disambiguationIndex = '';
710
+ const valueFieldAlias = processColumnReference((currentAgg.valueField || 'count'), databaseType, undefined, false, true);
711
+ const valueAliasSubstring = currentAgg.valueField
712
+ ? `${processColumnReference(currentAgg.valueField, databaseType, undefined, true)} AS ${valueFieldAlias}`
713
+ : '';
714
+ let value2AliasSubstring = '';
715
+ const disambiguation = pivot.aggregations?.length > 1
716
+ ? `${disambiguationIndex}_${currentAgg.aggregationType}`
717
+ : '';
718
+ // If the field type is bool, wrap it in a CASE WHEN
719
+ let valueExpr = !currentAgg.valueField ? '*' : valueFieldAlias;
720
+ valueExpr = currentAgg.valueFieldType === 'bool'
721
+ ? `CASE WHEN ${valueFieldAlias} THEN 1 ELSE 0 END`
722
+ : valueExpr;
723
+ if (currentAgg.aggregationType === 'percentage') {
724
+ if (!currentAgg.valueField) {
725
+ throw new Error('No value field provided for percentage aggregation');
726
+ }
727
+ const valueField2Alias = processColumnReference(currentAgg.valueField2 ?? currentAgg.valueField, databaseType, undefined, false, true);
728
+ const value2Expr = (currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool'
729
+ ? `CASE WHEN ${valueField2Alias} THEN 1 ELSE 0 END`
730
+ : valueField2Alias;
731
+ value2AliasSubstring = currentAgg.valueField2 && currentAgg.valueField !== currentAgg.valueField2
732
+ ? `${processColumnReference(currentAgg.valueField2, databaseType, undefined, true)} AS ${valueField2Alias}`
733
+ : '';
734
+ const percentageExpr = currentAgg.valueField === currentAgg.valueField2 || !currentAgg.valueField2
735
+ ? `sum(${valueExpr}) / ${(currentAgg.valueField2Type ?? currentAgg.valueFieldType) === 'bool' ? 'COUNT(*)' : 'SUM(sum(' + valueExpr + ')) OVER ()'}`
736
+ : `sum(${valueExpr}) / GREATEST(sum(${value2Expr}), 1)`;
737
+ quillAggSelects = [
738
+ ...quillAggSelects,
739
+ `${percentageExpr} as ${processColumnReference(`${currentAgg.valueField ?? ''}${disambiguation}`, databaseType, undefined, false, true)}`,
740
+ ];
741
+ }
742
+ else {
743
+ quillAggSelects = [
744
+ ...quillAggSelects,
745
+ `${processAggType(currentAgg.aggregationType)}(${valueExpr}) AS ${processColumnReference((currentAgg.valueField || 'count') + disambiguation, databaseType)}`,
746
+ ];
747
+ }
748
+ if (valueAliasSubstring)
749
+ valueFieldAliases.push(valueAliasSubstring);
750
+ if (value2AliasSubstring)
751
+ valueFieldAliases.push(value2AliasSubstring);
752
+ });
753
+ valueFieldAliases = Array.from(new Set(valueFieldAliases));
754
+ const sortQuery = pivot.sort && pivot.sortField && pivot.rowLimit
755
+ ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType)} ${pivot.sortDirection || ''} `
756
+ : '';
757
+ const pivotQuery = `, quill_alias AS (
758
+ SELECT ${valueFieldAliases.join(', ')} FROM quill_base_table
759
+ ),
760
+ quill_qt_agg AS (
761
+ SELECT ${quillAggSelects.join(', ')} FROM quill_alias
762
+ ),
763
+ quill_base_pivot AS (
764
+ SELECT ${pivot.rowLimit && databaseType.toLowerCase() === 'mssql'
765
+ ? `TOP ${pivot.rowLimit}`
766
+ : ''} * FROM quill_qt_agg qt
767
+ ${sortQuery}${pivot.rowLimit && databaseType.toLowerCase() !== 'mssql'
768
+ ? ` LIMIT ${pivot.rowLimit}`
769
+ : ''}
770
+ )
771
+ SELECT * FROM quill_base_pivot`
494
772
  .replace(/\s+/g, ' ')
495
773
  .trim();
496
774
  return itemQuery.replace(/SELECT \* FROM\s+["'[`]?quill_base_table["'\]`]?\s*$/, pivotQuery);
@@ -502,19 +780,19 @@ export function additionalProcessingOnPivotQuery(pivot, query, additionalProcess
502
780
  if (!isValidBaseQuery) {
503
781
  return undefined;
504
782
  }
783
+ if (!pivot.aggregations || pivot.aggregations.length === 0) {
784
+ if (pivot.aggregationType) {
785
+ pivot.aggregations = [{ aggregationType: pivot.aggregationType, valueField: pivot.valueField, valueField2: pivot.valueField2 }];
786
+ }
787
+ else {
788
+ throw new Error('No aggregations provided for pivot');
789
+ }
790
+ }
505
791
  let rowsPerPage = 0;
506
792
  let currentInterval = 0;
507
793
  let offset = 0;
508
794
  let limit = 1000;
509
- let sortQuery = `${pivot.sort && pivot.sortField
510
- ? ` ORDER BY ${processColumnReference(pivot.sortField, databaseType, undefined, true)} ${pivot.sortDirection || ''}`
511
- : pivot.rowField
512
- ? `ORDER BY ${processColumnReference(`${pivot.rowField}`, databaseType)}`
513
- : pivot.valueField
514
- ? pivot.aggregationType === 'percentage'
515
- ? `ORDER BY ${processColumnReference(`${pivot.valueField}_${matchCasing('percentage', pivot.valueField)}`, databaseType, undefined, false, true)}`
516
- : `ORDER BY ${processColumnReference(pivot.valueField, databaseType, undefined, false, true)}`
517
- : ''}`;
795
+ let sortQuery = '';
518
796
  if (additionalProcessing.page) {
519
797
  const page = additionalProcessing.page.page || 0;
520
798
  if (additionalProcessing.page.rowsPerRequest) {
@@ -524,9 +802,18 @@ export function additionalProcessingOnPivotQuery(pivot, query, additionalProcess
524
802
  currentInterval = page ? Math.floor(page / (limit / rowsPerPage)) : 0;
525
803
  offset = currentInterval * limit;
526
804
  }
805
+ const disambiguation = pivot.aggregations.length > 1
806
+ ? `_${matchCasing(pivot.aggregations?.[0]?.aggregationType, pivot.aggregations?.[0]?.valueField)}`
807
+ : '';
527
808
  if (additionalProcessing.sort) {
528
809
  sortQuery = `ORDER BY ${processColumnReference(additionalProcessing.sort.field, databaseType)} ${additionalProcessing.sort.direction || ''}`;
529
810
  }
811
+ else {
812
+ const valueFieldAlias = processColumnReference((pivot.aggregations?.[0]?.valueField ?? '') + disambiguation, databaseType, undefined, false, true);
813
+ const defaultSortField = pivot.sortField || pivot.rowField || valueFieldAlias;
814
+ const defaultSortDirection = pivot.sortDirection || '';
815
+ sortQuery = `ORDER BY ${processColumnReference(defaultSortField, databaseType)} ${defaultSortDirection}`;
816
+ }
530
817
  const additionalProcessingQuery = `
531
818
  SELECT *
532
819
  FROM quill_base_pivot ${sortQuery}${databaseType === 'mssql' ? ` OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY` : ` LIMIT ${limit} OFFSET ${offset}`}