@quillsql/react 2.10.39 → 2.11.0

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 (123) hide show
  1. package/dist/cjs/Chart.d.ts +4 -0
  2. package/dist/cjs/Chart.d.ts.map +1 -1
  3. package/dist/cjs/Chart.js +5 -5
  4. package/dist/cjs/ChartBuilder.js +2 -2
  5. package/dist/cjs/Context.d.ts +1 -1
  6. package/dist/cjs/Context.d.ts.map +1 -1
  7. package/dist/cjs/Context.js +3 -1
  8. package/dist/cjs/Dashboard.d.ts +3 -1
  9. package/dist/cjs/Dashboard.d.ts.map +1 -1
  10. package/dist/cjs/Dashboard.js +4 -4
  11. package/dist/cjs/QuillProvider.d.ts +3 -1
  12. package/dist/cjs/QuillProvider.d.ts.map +1 -1
  13. package/dist/cjs/QuillProvider.js +2 -2
  14. package/dist/cjs/ReportBuilder.d.ts +40 -40
  15. package/dist/cjs/ReportBuilder.d.ts.map +1 -1
  16. package/dist/cjs/ReportBuilder.js +2026 -917
  17. package/dist/cjs/components/Chart/LineChart.d.ts +5 -1
  18. package/dist/cjs/components/Chart/LineChart.d.ts.map +1 -1
  19. package/dist/cjs/components/Chart/LineChart.js +18 -6
  20. package/dist/cjs/components/QuillTable.d.ts +1 -1
  21. package/dist/cjs/components/QuillTable.d.ts.map +1 -1
  22. package/dist/cjs/components/QuillTable.js +157 -157
  23. package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
  24. package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
  25. package/dist/cjs/components/ReportBuilder/AddColumnPopover.js +128 -0
  26. package/dist/cjs/components/ReportBuilder/ast.d.ts +512 -0
  27. package/dist/cjs/components/ReportBuilder/ast.d.ts.map +1 -0
  28. package/dist/cjs/components/ReportBuilder/ast.js +210 -0
  29. package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts +7 -0
  30. package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
  31. package/dist/cjs/components/ReportBuilder/bigDateMap.js +689 -0
  32. package/dist/cjs/components/ReportBuilder/constants.d.ts +89 -0
  33. package/dist/cjs/components/ReportBuilder/constants.d.ts.map +1 -0
  34. package/dist/cjs/components/ReportBuilder/constants.js +130 -0
  35. package/dist/cjs/components/ReportBuilder/convert.d.ts +41 -0
  36. package/dist/cjs/components/ReportBuilder/convert.d.ts.map +1 -0
  37. package/dist/cjs/components/ReportBuilder/convert.js +730 -0
  38. package/dist/cjs/components/ReportBuilder/operators.d.ts +445 -0
  39. package/dist/cjs/components/ReportBuilder/operators.d.ts.map +1 -0
  40. package/dist/cjs/components/ReportBuilder/operators.js +552 -0
  41. package/dist/cjs/components/ReportBuilder/pivot.d.ts +10 -0
  42. package/dist/cjs/components/ReportBuilder/pivot.d.ts.map +1 -0
  43. package/dist/cjs/components/ReportBuilder/pivot.js +2 -0
  44. package/dist/cjs/components/ReportBuilder/postgres.d.ts +150 -0
  45. package/dist/cjs/components/ReportBuilder/postgres.d.ts.map +1 -0
  46. package/dist/cjs/components/ReportBuilder/postgres.js +365 -0
  47. package/dist/cjs/components/ReportBuilder/schema.d.ts +23 -0
  48. package/dist/cjs/components/ReportBuilder/schema.d.ts.map +1 -0
  49. package/dist/cjs/components/ReportBuilder/schema.js +2 -0
  50. package/dist/cjs/components/ReportBuilder/ui.d.ts +34 -0
  51. package/dist/cjs/components/ReportBuilder/ui.d.ts.map +1 -0
  52. package/dist/cjs/components/ReportBuilder/ui.js +389 -0
  53. package/dist/cjs/components/ReportBuilder/util.d.ts +76 -0
  54. package/dist/cjs/components/ReportBuilder/util.d.ts.map +1 -0
  55. package/dist/cjs/components/ReportBuilder/util.js +648 -0
  56. package/dist/cjs/components/UiComponents.d.ts +15 -2
  57. package/dist/cjs/components/UiComponents.d.ts.map +1 -1
  58. package/dist/cjs/components/UiComponents.js +50 -3
  59. package/dist/cjs/utils/crypto.d.ts +1 -1
  60. package/dist/cjs/utils/crypto.d.ts.map +1 -1
  61. package/dist/cjs/utils/crypto.js +9 -5
  62. package/dist/esm/Chart.d.ts +4 -0
  63. package/dist/esm/Chart.d.ts.map +1 -1
  64. package/dist/esm/Chart.js +5 -5
  65. package/dist/esm/ChartBuilder.js +1 -1
  66. package/dist/esm/Context.d.ts +1 -1
  67. package/dist/esm/Context.d.ts.map +1 -1
  68. package/dist/esm/Context.js +3 -1
  69. package/dist/esm/Dashboard.d.ts +3 -1
  70. package/dist/esm/Dashboard.d.ts.map +1 -1
  71. package/dist/esm/Dashboard.js +4 -4
  72. package/dist/esm/QuillProvider.d.ts +3 -1
  73. package/dist/esm/QuillProvider.d.ts.map +1 -1
  74. package/dist/esm/QuillProvider.js +2 -2
  75. package/dist/esm/ReportBuilder.d.ts +40 -40
  76. package/dist/esm/ReportBuilder.d.ts.map +1 -1
  77. package/dist/esm/ReportBuilder.js +2028 -917
  78. package/dist/esm/components/Chart/LineChart.d.ts +5 -1
  79. package/dist/esm/components/Chart/LineChart.d.ts.map +1 -1
  80. package/dist/esm/components/Chart/LineChart.js +18 -6
  81. package/dist/esm/components/QuillTable.d.ts +1 -1
  82. package/dist/esm/components/QuillTable.d.ts.map +1 -1
  83. package/dist/esm/components/QuillTable.js +157 -157
  84. package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
  85. package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
  86. package/dist/esm/components/ReportBuilder/AddColumnPopover.js +125 -0
  87. package/dist/esm/components/ReportBuilder/ast.d.ts +512 -0
  88. package/dist/esm/components/ReportBuilder/ast.d.ts.map +1 -0
  89. package/dist/esm/components/ReportBuilder/ast.js +186 -0
  90. package/dist/esm/components/ReportBuilder/bigDateMap.d.ts +7 -0
  91. package/dist/esm/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
  92. package/dist/esm/components/ReportBuilder/bigDateMap.js +686 -0
  93. package/dist/esm/components/ReportBuilder/constants.d.ts +89 -0
  94. package/dist/esm/components/ReportBuilder/constants.d.ts.map +1 -0
  95. package/dist/esm/components/ReportBuilder/constants.js +127 -0
  96. package/dist/esm/components/ReportBuilder/convert.d.ts +41 -0
  97. package/dist/esm/components/ReportBuilder/convert.d.ts.map +1 -0
  98. package/dist/esm/components/ReportBuilder/convert.js +719 -0
  99. package/dist/esm/components/ReportBuilder/operators.d.ts +445 -0
  100. package/dist/esm/components/ReportBuilder/operators.d.ts.map +1 -0
  101. package/dist/esm/components/ReportBuilder/operators.js +548 -0
  102. package/dist/esm/components/ReportBuilder/pivot.d.ts +10 -0
  103. package/dist/esm/components/ReportBuilder/pivot.d.ts.map +1 -0
  104. package/dist/esm/components/ReportBuilder/pivot.js +1 -0
  105. package/dist/esm/components/ReportBuilder/postgres.d.ts +150 -0
  106. package/dist/esm/components/ReportBuilder/postgres.d.ts.map +1 -0
  107. package/dist/esm/components/ReportBuilder/postgres.js +355 -0
  108. package/dist/esm/components/ReportBuilder/schema.d.ts +23 -0
  109. package/dist/esm/components/ReportBuilder/schema.d.ts.map +1 -0
  110. package/dist/esm/components/ReportBuilder/schema.js +1 -0
  111. package/dist/esm/components/ReportBuilder/ui.d.ts +34 -0
  112. package/dist/esm/components/ReportBuilder/ui.d.ts.map +1 -0
  113. package/dist/esm/components/ReportBuilder/ui.js +366 -0
  114. package/dist/esm/components/ReportBuilder/util.d.ts +76 -0
  115. package/dist/esm/components/ReportBuilder/util.d.ts.map +1 -0
  116. package/dist/esm/components/ReportBuilder/util.js +616 -0
  117. package/dist/esm/components/UiComponents.d.ts +15 -2
  118. package/dist/esm/components/UiComponents.d.ts.map +1 -1
  119. package/dist/esm/components/UiComponents.js +47 -2
  120. package/dist/esm/utils/crypto.d.ts +1 -1
  121. package/dist/esm/utils/crypto.d.ts.map +1 -1
  122. package/dist/esm/utils/crypto.js +9 -5
  123. package/package.json +1 -1
@@ -0,0 +1,719 @@
1
+ import { extractColumnish, isSelect, isLogicalBinaryExpr, isBigQuery, isNumericColumnType, } from './ast';
2
+ import { generateColumnExpr, isColumnDateish } from './util';
3
+ // Helper functions that preprocess the AST.
4
+ // Find and replace certain pre-set date comparison queries with their
5
+ // more stable counterparts.
6
+ // TODO: implement me
7
+ // eslint-disable-next-line no-unused-vars
8
+ export function convertDateComparison(node) {
9
+ function searchAndReplace(obj) {
10
+ if (Array.isArray(obj)) {
11
+ // If the current node is an array, process each element of the array
12
+ obj.forEach((element) => {
13
+ searchAndReplace(element);
14
+ });
15
+ }
16
+ else if (typeof obj === 'object' && obj !== null) {
17
+ // "Created in the past 90 days" should be written in SQL as:
18
+ // "created_at" >= date_trunc('day', CURRENT_DATE) - INTERVAL '90 days';
19
+ // "created_at" >= date_trunc('month', CURRENT_DATE) - INTERVAL '6 months';
20
+ // TODO: add more matchers here
21
+ }
22
+ else {
23
+ // If the current object does not match the pattern, continue searching its properties
24
+ Object.values(obj).forEach((value) => {
25
+ searchAndReplace(value);
26
+ });
27
+ }
28
+ }
29
+ // Start the recursive search and replace from the root node
30
+ searchAndReplace(node);
31
+ return node;
32
+ }
33
+ // THIS IS ONLY USEFUL TO CONVERT FUNCTIONS INTO OTHER FUNCTIONS FOR BIGQUERY FOR FILTERS
34
+ export function recursiveSearchAndReplace(node, search, replace) {
35
+ if (typeof node !== 'object') {
36
+ return;
37
+ }
38
+ if (node) {
39
+ Object.keys(node).forEach((key) => {
40
+ recursiveSearchAndReplace(node[key], search, replace);
41
+ if (node[key] === search) {
42
+ node[key] = replace;
43
+ }
44
+ });
45
+ }
46
+ }
47
+ /**
48
+ * Takes a bigquery AST as input, cleans it, and returns a Select AST.
49
+ */
50
+ export function convertBigQuery(ast) {
51
+ // For BigQuery, the info we want is nested inside the 'select' key
52
+ if (isBigQuery(ast)) {
53
+ let newAst = ast.select;
54
+ newAst.from?.forEach((tbl) => {
55
+ tbl.table = tbl.table.replaceAll('`', '');
56
+ });
57
+ if (newAst.columns && Array.isArray(newAst.columns)) {
58
+ newAst.columns.forEach((col) => {
59
+ if (col.expr && !col.type) {
60
+ col.type = 'expr';
61
+ }
62
+ });
63
+ }
64
+ // Look through every node in the AST convert every instance of DATE_SUB to TIMESTAMP_SUB
65
+ recursiveSearchAndReplace(newAst.where, 'DATE_SUB', 'TIMESTAMP_SUB');
66
+ recursiveSearchAndReplace(newAst.where, 'CURRENT_DATE', 'CURRENT_TIMESTAMP');
67
+ recursiveSearchAndReplace(newAst.where, 'DATE_TRUNC', 'TIMESTAMP_TRUNC');
68
+ recursiveSearchAndReplace(newAst.where, 'DATE', 'TIMESTAMP');
69
+ // TODO: handle joins nicely
70
+ if (newAst.from?.length > 1) {
71
+ newAst.from = [newAst.from[0]];
72
+ }
73
+ // TODO: handle UNION ALL joins
74
+ if (newAst._next) {
75
+ delete newAst._next;
76
+ }
77
+ return newAst;
78
+ }
79
+ else {
80
+ return ast;
81
+ }
82
+ }
83
+ /**
84
+ * Replaces the wildcard column selector with their literal column names.
85
+ */
86
+ export function convertWildcardColumns(ast, schemaTables) {
87
+ if (ast.columns !== '*' && ast.columns.length !== 0)
88
+ return ast;
89
+ if (!ast.from)
90
+ return ast;
91
+ // Map SELECT(*) to their actual columns
92
+ const tableNamesInQuery = ast.from.map((tbl) => tbl.table);
93
+ const allColumns = schemaTables
94
+ .filter((t) => tableNamesInQuery.includes(t.displayName))
95
+ .flatMap((table) => table.columns);
96
+ return {
97
+ ...ast,
98
+ columns: allColumns.map((column) => ({
99
+ type: 'expr',
100
+ expr: {
101
+ type: 'column_ref',
102
+ table: null,
103
+ column: column.name,
104
+ },
105
+ as: null,
106
+ })),
107
+ };
108
+ }
109
+ /**
110
+ * Processes the AST and builds up a new pivot, given the old AST, the old pivot
111
+ * and a list of tables in the schema.
112
+ */
113
+ export function convertGroupBy(ast, prevPivot = null, schemaTables) {
114
+ if (!isSelect(ast))
115
+ return { ast, pivot: prevPivot };
116
+ let newAst = {};
117
+ let pivot = {};
118
+ let newColumns = [];
119
+ let aliasMap = {};
120
+ const tableNamesInQuery = (ast.from ?? []).map((tbl) => {
121
+ // This assumes that all the entries in the from clause are BaseFrom.
122
+ // TODO: Handle Join, TableExpr, and Dual (maybe not the last one).
123
+ return tbl.table;
124
+ });
125
+ const columns = schemaTables
126
+ .filter((t) => tableNamesInQuery.includes(t.displayName))
127
+ .flatMap((table) => table.columns)
128
+ .map((column) => ({
129
+ field: column.displayName,
130
+ fieldType: column.fieldType,
131
+ }));
132
+ // If there is not GROUP BY clause in the AST, we still need to process the
133
+ // AST but it should pass-through in nature.
134
+ if (!ast.groupby) {
135
+ // If there was a pivot before, evaluate the pivot.
136
+ if (prevPivot) {
137
+ // Make sure that the AST includes all the columns on the prevPivot
138
+ const columnAlreadyExists = (col) => ast.columns.find((c) => [c.expr?.value, c.expr?.column, c.as].includes(col));
139
+ if (prevPivot.valueField) {
140
+ const existingCol = columnAlreadyExists(prevPivot.valueField);
141
+ newColumns.push(existingCol ?? generateColumnExpr(prevPivot.valueField));
142
+ }
143
+ if (prevPivot.rowField) {
144
+ const existingCol = columnAlreadyExists(prevPivot.rowField);
145
+ newColumns.push(existingCol ?? generateColumnExpr(prevPivot.rowField));
146
+ }
147
+ if (prevPivot.columnField) {
148
+ const existingCol = columnAlreadyExists(prevPivot.columnField);
149
+ newColumns.push(existingCol ?? generateColumnExpr(prevPivot.columnField));
150
+ }
151
+ ast.columns = newColumns;
152
+ return { pivot: prevPivot, ast };
153
+ }
154
+ for (let i = 0; i < ast.columns.length; i++) {
155
+ const column = ast.columns[i];
156
+ // Extract out the column name, note that there are several ways
157
+ // that a column node could be structured so we need to check.
158
+ let columnName;
159
+ let columnAs = null;
160
+ let columnTable = null;
161
+ if (column.type === 'expr') {
162
+ columnAs = column.as;
163
+ columnTable = column.expr.table;
164
+ if (column.expr.type === 'column_ref') {
165
+ columnName = column.expr.column;
166
+ }
167
+ else if (column.expr.type === 'string') {
168
+ columnName = column.expr.value;
169
+ }
170
+ else if (column.expr.type === 'double_quote_string') {
171
+ columnName = column.expr?.value;
172
+ }
173
+ else if (column.expr?.args?.expr) {
174
+ columnName = column.expr?.args?.expr?.value;
175
+ }
176
+ if (column.expr.type === 'aggr_func') {
177
+ pivot.aggregationType = column.expr.name.toLowerCase();
178
+ pivot.valueField = column.expr.args?.expr?.value;
179
+ if (column.expr.args.expr.type === 'column_ref') {
180
+ columnTable = column.expr.args.expr.table;
181
+ columnName = column.expr.args.expr.column;
182
+ }
183
+ else if (column.expr.args.expr.type === 'double_quote_string') {
184
+ columnName = column.expr.args.expr?.value;
185
+ }
186
+ else if (column.expr?.args?.expr) {
187
+ columnName = column.expr?.args?.expr?.value;
188
+ }
189
+ }
190
+ }
191
+ if (columnName === '*') {
192
+ // TODO: make valueField be a number
193
+ pivot.valueField = columns[0].field;
194
+ columnName = columns[0].field;
195
+ }
196
+ newColumns.push({
197
+ type: 'expr',
198
+ expr: {
199
+ type: 'column_ref',
200
+ table: columnTable,
201
+ column: columnName,
202
+ },
203
+ as: columnAs,
204
+ });
205
+ }
206
+ newAst = { ...ast };
207
+ newAst.columns = newColumns;
208
+ const newPivot = Object.keys(pivot).length === 0 ? null : pivot;
209
+ return { pivot: newPivot, ast: newAst };
210
+ }
211
+ // Iterate over columns to find aggr_func -> set pivot.aggregationType and pivot.valueField
212
+ for (let i = 0; i < ast.columns.length; i++) {
213
+ const column = ast.columns[i];
214
+ const field = columns.find((c) => c.field === column.expr?.args?.expr?.column);
215
+ if (column?.expr?.type === 'aggr_func') {
216
+ // CASE WHEN
217
+ if (column.expr?.args?.expr?.type.toLowerCase() === 'case' &&
218
+ ['double_quote_string', 'column_ref'].includes(column.expr?.args?.expr?.args[0]?.result?.type)) {
219
+ // The result of the CASE is the column to use
220
+ const columnNode = column.expr?.args?.expr?.args[0]?.result;
221
+ const columnName = columnNode?.value ?? columnNode?.column;
222
+ pivot.valueField = columnName;
223
+ pivot.aggregationType = column?.expr?.name?.toLowerCase();
224
+ const findByName = (x) => [x.expr?.value, x.expr?.column].includes(columnName);
225
+ if (!newColumns.find(findByName)) {
226
+ newColumns.push({
227
+ type: 'expr',
228
+ expr: {
229
+ type: 'column_ref',
230
+ table: null,
231
+ column: columnName,
232
+ },
233
+ as: null,
234
+ });
235
+ }
236
+ // We need to extract a column name from the condition expression of
237
+ // the CASE and we'll use that as the columnField in our pivot.
238
+ const condNode = column.expr?.args?.expr?.args[0]?.cond;
239
+ const condColumnName = condNode.left?.column ?? condNode.left?.value;
240
+ pivot.columnField = condColumnName;
241
+ const findByColName = (x) => [x.expr?.value, x.expr?.column].includes(condColumnName);
242
+ if (!newColumns.find(findByColName)) {
243
+ // Make sure the column we extracted is in our column list.
244
+ newColumns.push({
245
+ type: 'expr',
246
+ expr: {
247
+ type: 'column_ref',
248
+ table: null,
249
+ column: condColumnName,
250
+ },
251
+ as: null,
252
+ });
253
+ }
254
+ // REGULAR
255
+ }
256
+ else if (
257
+ // sum("amount")
258
+ column.expr?.args?.expr?.type === 'double_quote_string' ||
259
+ // sum(amount)
260
+ column.expr?.args?.expr?.type === 'column_ref' ||
261
+ // count(*)
262
+ column.expr?.args?.expr?.type === 'star') {
263
+ console.log(column);
264
+ const columnNode = column.expr?.args?.expr;
265
+ const columnName = columnNode?.value || columnNode?.column;
266
+ // if count(*), make the value field an actual column
267
+ if (columnName === '*') {
268
+ // TODO: make valueField be a number
269
+ pivot.valueField = columns[0].field;
270
+ pivot.aggregationType = column?.expr?.name.toLowerCase();
271
+ const findByName = (x) => [x.expr?.value, x.expr?.column].includes(columns[0].field);
272
+ if (!newColumns.find(findByName)) {
273
+ newColumns.push({
274
+ type: 'expr',
275
+ expr: {
276
+ type: 'column_ref',
277
+ table: null,
278
+ column: columns[0].field,
279
+ },
280
+ as: null,
281
+ });
282
+ }
283
+ }
284
+ else {
285
+ // ONLY SET VALUE FIELD IF THE FIELD TYPE IS A NUMBER
286
+ if (isNumericColumnType(field?.fieldType)) {
287
+ pivot.valueField = columnName;
288
+ pivot.aggregationType = column?.expr?.name.toLowerCase();
289
+ }
290
+ const findByName = (x) => [x.expr?.value, x.expr?.column].includes(columnName);
291
+ if (!newColumns.find(findByName)) {
292
+ newColumns.push({
293
+ type: 'expr',
294
+ expr: {
295
+ type: 'column_ref',
296
+ table: null,
297
+ column: columnName,
298
+ },
299
+ as: null,
300
+ });
301
+ }
302
+ }
303
+ }
304
+ else {
305
+ // normal column?
306
+ const columnName = column.expr?.column || column.expr?.value;
307
+ const findByName = (x) => [x.expr?.value, x.expr?.column].includes(columnName);
308
+ if (!newColumns.find(findByName)) {
309
+ newColumns.push(column);
310
+ }
311
+ }
312
+ }
313
+ else if (column.expr?.type === 'function') {
314
+ // date_trunc("month", "created_at") as month
315
+ if (column.type === 'expr' &&
316
+ column.expr?.type.toLowerCase() === 'function' &&
317
+ column.expr?.name.toLowerCase() === 'date_trunc' &&
318
+ column.expr?.args?.type === 'expr_list' &&
319
+ column.as) {
320
+ const columnObj = column.expr?.args?.value[1];
321
+ const periodObj = column.expr?.args?.value[0];
322
+ aliasMap[column.as] = columnObj?.value ?? columnObj.column;
323
+ newColumns.push({
324
+ type: 'expr',
325
+ expr: {
326
+ type: 'function',
327
+ name: 'DATE_TRUNC',
328
+ args: {
329
+ type: 'expr_list',
330
+ value: [
331
+ { type: 'single_quote_string', value: periodObj?.value },
332
+ {
333
+ type: 'column_ref',
334
+ table: null,
335
+ column: columnObj?.value ?? columnObj.column,
336
+ },
337
+ ],
338
+ },
339
+ },
340
+ as: columnObj?.value ?? columnObj.column,
341
+ });
342
+ }
343
+ else if (column.type === 'expr' &&
344
+ column.expr?.type.toLowerCase() === 'function' &&
345
+ column.expr?.name.toLowerCase() === 'date' &&
346
+ column.expr?.args?.type === 'expr_list' &&
347
+ column.as) {
348
+ const columnObj = column.expr?.args?.value[0];
349
+ aliasMap[column.as] = columnObj?.value ?? columnObj.column;
350
+ newColumns.push({
351
+ type: 'expr',
352
+ expr: {
353
+ type: 'function',
354
+ name: 'DATE_TRUNC',
355
+ args: {
356
+ type: 'expr_list',
357
+ value: [
358
+ { type: 'single_quote_string', value: 'day' },
359
+ {
360
+ type: 'column_ref',
361
+ table: null,
362
+ column: columnObj?.value ?? columnObj.column,
363
+ },
364
+ ],
365
+ },
366
+ },
367
+ as: columnObj?.value ?? columnObj.column,
368
+ });
369
+ }
370
+ else {
371
+ newColumns.push(column);
372
+ }
373
+ }
374
+ else if (column.type === 'expr' &&
375
+ column.expr?.type.toLowerCase() === 'extract' &&
376
+ column.as) {
377
+ const cast_type = column.expr?.args.cast_type;
378
+ const source = column.expr?.args.source;
379
+ const field = column.expr?.args.field;
380
+ aliasMap[column.as] = source?.value ?? source.column;
381
+ newColumns.push({
382
+ type: 'expr',
383
+ expr: {
384
+ type: 'extract',
385
+ args: { field, cast_type, source },
386
+ },
387
+ as: source?.value ?? source.column,
388
+ });
389
+ }
390
+ else {
391
+ const columnNode = column.expr;
392
+ newColumns.push({
393
+ type: 'expr',
394
+ expr: {
395
+ type: 'column_ref',
396
+ table: columnNode.table,
397
+ column: columnNode.args?.length
398
+ ? columnNode.args?.value.find((elem) => ['double_quote_string', 'column_ref'].includes(elem.type))?.value
399
+ : columnNode.column ?? columnNode?.value,
400
+ },
401
+ as: null,
402
+ });
403
+ }
404
+ }
405
+ // Look for a date_trunc in the GROUP BY and set it to the rowField.
406
+ // If there are no date_trunc in the GROUP BY, we also need to check
407
+ // to see if there was a date_trunc column that was given an alias and
408
+ // that alias is used in the GROUP BY (which is also set to rowField).
409
+ const isDateTrunc = (item) => ['function', 'date_trunc'].includes(item.type);
410
+ const isExtract = (item) => 'extract' === item.type;
411
+ const isCol = (x) => ['double_quote_string', 'column_ref'].includes(x.type);
412
+ const isAlias = (x) => isCol(x) && aliasMap[extractColumnish(x)];
413
+ const dateTruncGroup = ast.groupby.find(isDateTrunc);
414
+ const extractGroup = ast.groupby.find(isExtract);
415
+ if (dateTruncGroup) {
416
+ const column = dateTruncGroup.args?.value.find(isCol);
417
+ pivot.rowField = extractColumnish(column);
418
+ }
419
+ else if (extractGroup) {
420
+ const column = extractGroup.args?.source;
421
+ pivot.rowField = extractColumnish(column);
422
+ }
423
+ else if (aliasMap) {
424
+ const match = ast.groupby.find(isAlias);
425
+ const matchColumnish = extractColumnish(match);
426
+ if (matchColumnish) {
427
+ const actualColumnName = aliasMap[matchColumnish];
428
+ pivot.rowField = actualColumnName;
429
+ }
430
+ }
431
+ // Otherwise, set the first groupby to rowField then set
432
+ // next field to columnField (if there is one)
433
+ for (let j = 0; j < ast.groupby.length; j++) {
434
+ const group = ast.groupby[j];
435
+ let groupValue = extractColumnish(group);
436
+ // check if this value is an alias, if so use concrete value
437
+ if (groupValue && aliasMap[groupValue]) {
438
+ groupValue = aliasMap[groupValue];
439
+ }
440
+ // Try to set the rowField and columnField.
441
+ if (!pivot.rowField) {
442
+ pivot.rowField = groupValue;
443
+ }
444
+ else if (!pivot.columnField && groupValue !== pivot.rowField) {
445
+ pivot.columnField = groupValue;
446
+ }
447
+ }
448
+ newAst = { ...ast };
449
+ newAst.groupby = null;
450
+ newAst.columns = newColumns;
451
+ // Automatically order by the rowField if this is a 1D pivot (ie. no columnField)
452
+ // we also want to automatically order by rowField if the rowField is date-ish.
453
+ // TODO: dateish check breaks for plain date-type columns (ie. non date_trunc/extract)
454
+ const isPivot1D = pivot.rowField && !pivot.columnField;
455
+ const isRowFieldDateish = isColumnDateish(newAst.columns.find((c) => c.as === pivot?.rowField));
456
+ const isAutoOrderBy = isPivot1D || isRowFieldDateish;
457
+ newAst.orderby = isAutoOrderBy
458
+ ? [
459
+ {
460
+ expr: { type: 'column_ref', table: null, column: pivot.rowField },
461
+ type: newAst.orderby !== null
462
+ ? newAst.orderby[0]?.type ?? 'DESC'
463
+ : 'DESC',
464
+ },
465
+ ]
466
+ : null;
467
+ // If the AI didn't generate an aggregate, add one.
468
+ if (!pivot.valueField) {
469
+ pivot.valueField = columns[0].field;
470
+ pivot.aggregationType = 'sum';
471
+ }
472
+ return { pivot, ast: newAst };
473
+ }
474
+ /**
475
+ * Applies a pivot to an object with {rows, fields} keys, returning
476
+ * a new object with the aggragation applied to the rows and the
477
+ * corresponding fields updated to reflect the accurate types.
478
+ */
479
+ export function applyPivot(data, pivot, schema) {
480
+ const newRows = [];
481
+ const { rows, fields } = data;
482
+ const { aggregationType, valueField, rowField, columnField } = pivot;
483
+ if (rows.length === 0)
484
+ return { rows, fields };
485
+ if ((rowField || columnField) && !valueField)
486
+ return { rows, fields };
487
+ if (columnField && !Object.keys(schema).includes(columnField))
488
+ return { rows, fields };
489
+ const AGGREGATORS = {
490
+ sum: (prev, next, _count = 0) => Number(prev) + Number(next),
491
+ max: (prev, next, _count = 0) => Math.max(Number(prev), Number(next)),
492
+ min: (prev, next, _count = 0) => Math.min(Number(prev), Number(next)),
493
+ count: (_prev, _next, count = 0) => Number(count) + 1,
494
+ avg: (prev, next, count = 0) => (Number(prev) * count + Number(next)) / (count + 1),
495
+ };
496
+ if (!columnField) {
497
+ // Simple 1D pivot
498
+ const keys = Object.keys(rows[0]);
499
+ for (const row of rows) {
500
+ let newRow = newRows.find((r) => r[rowField] === row[rowField]);
501
+ if (newRow) {
502
+ // Aggregate the existing row with this row
503
+ newRow[valueField] = AGGREGATORS[aggregationType](newRow[valueField], row[valueField], newRow.count ?? 0);
504
+ newRow.count = (newRow.count ?? 0) + 1;
505
+ }
506
+ else {
507
+ // Build up a new row and add it to our new rows array.
508
+ newRow = keys.reduce((obj, key) => {
509
+ obj[key] = row[key];
510
+ return obj;
511
+ }, {});
512
+ newRow.count = 1;
513
+ newRows.push(newRow);
514
+ }
515
+ }
516
+ // Remove the count property before returning
517
+ for (const row of newRows) {
518
+ delete row.count;
519
+ }
520
+ return { rows: newRows, fields };
521
+ }
522
+ else {
523
+ // 2D Pivot with rowField and columnField
524
+ const columnVariants = Object.keys(schema[columnField]);
525
+ const rowFieldColumnType = fields.find((f) => f.name === rowField);
526
+ const valueFieldColumnType = fields.find((f) => f.name === valueField);
527
+ const newFields = [
528
+ rowFieldColumnType,
529
+ ...columnVariants.map((name) => {
530
+ return { ...valueFieldColumnType, name };
531
+ }),
532
+ ];
533
+ for (const row of rows) {
534
+ let newRow = newRows.find((r) => r[rowField] === row[rowField]);
535
+ if (newRow) {
536
+ // Aggregate the existing row with this row
537
+ newRow[row[columnField]] = AGGREGATORS[aggregationType](newRow[row[columnField]], row[valueField], newRow[`${row[columnField]}_count`] ?? 0);
538
+ newRow[`${row[columnField]}_count`] += 1;
539
+ }
540
+ else {
541
+ // Build up a new row and add it to our new rows array.
542
+ const defaultAllColumnVariants = columnVariants.reduce((obj, key) => {
543
+ // set each variant to the default value (eg. 0)
544
+ obj[key] = aggregationType === 'min' ? Number.MAX_VALUE : 0;
545
+ obj[`${key}_count`] = 0;
546
+ return obj;
547
+ }, {});
548
+ newRow = {
549
+ [rowField]: row[rowField],
550
+ ...defaultAllColumnVariants,
551
+ };
552
+ newRow[row[columnField]] =
553
+ aggregationType === 'count' ? 1 : row[valueField];
554
+ newRow[`${row[columnField]}_count`] = 1;
555
+ newRows.push(newRow);
556
+ }
557
+ }
558
+ // Remove the _count properties before returning
559
+ for (const row of newRows) {
560
+ for (const key in row) {
561
+ if (key.endsWith('_count')) {
562
+ // Show a special output for no entries so that we
563
+ // don't show our dummy defaults like 0 or Number.MAX_VALUE
564
+ if (row[key] === 0) {
565
+ row[key.replace('_count', '')] = '-';
566
+ }
567
+ delete row[key];
568
+ }
569
+ }
570
+ }
571
+ return { rows: newRows, fields: newFields };
572
+ }
573
+ }
574
+ export function convertStringComparison(node, databaseType) {
575
+ // Function to recursively search and replace the pattern in the object
576
+ function searchAndReplace(obj) {
577
+ if (Array.isArray(obj)) {
578
+ // If the current node is an array, process each element of the array
579
+ obj.forEach((element) => {
580
+ searchAndReplace(element);
581
+ });
582
+ }
583
+ else if (typeof obj === 'object' && obj !== null) {
584
+ // If the current node is an object, check for the pattern
585
+ if (obj.type === 'binary_expr' &&
586
+ (obj.operator === '=' ||
587
+ obj.operator === 'LIKE' ||
588
+ obj.operator === 'ILIKE') &&
589
+ obj.left &&
590
+ (obj.left.type === 'column_ref' ||
591
+ obj.left.type === 'double_quote_string') &&
592
+ obj.right &&
593
+ obj.right.type === 'single_quote_string') {
594
+ // Pattern found, modify the current object to use LOWER and LIKE
595
+ obj.operator = 'LIKE';
596
+ obj.left = {
597
+ type: 'function',
598
+ name: 'LOWER',
599
+ args: {
600
+ type: 'expr_list',
601
+ value: [
602
+ databaseType === 'BigQuery'
603
+ ? { type: 'column_ref', value: obj.left.column }
604
+ : { type: 'double_quote_string', value: obj.left.column },
605
+ ],
606
+ },
607
+ };
608
+ obj.right = {
609
+ type: 'function',
610
+ name: 'LOWER',
611
+ args: {
612
+ type: 'expr_list',
613
+ value: [
614
+ {
615
+ type: 'single_quote_string',
616
+ value: `%${obj.right.value.replaceAll('%', '')}%`,
617
+ },
618
+ ],
619
+ },
620
+ };
621
+ }
622
+ else if (obj.type === 'binary_expr' &&
623
+ (obj.operator === '!=' ||
624
+ obj.operator === 'NOT LIKE' ||
625
+ obj.operator === 'NOT ILIKE' ||
626
+ obj.operator === '<>') &&
627
+ obj.left &&
628
+ (obj.left.type === 'column_ref' ||
629
+ obj.left.type === 'double_quote_string') &&
630
+ obj.right &&
631
+ obj.right.type === 'single_quote_string') {
632
+ // Pattern found, modify the current object to use LOWER and LIKE
633
+ obj.operator = 'NOT LIKE';
634
+ obj.left = {
635
+ type: 'function',
636
+ name: 'LOWER',
637
+ args: {
638
+ type: 'expr_list',
639
+ value: [
640
+ databaseType === 'BigQuery'
641
+ ? { type: 'column_ref', value: obj.left.column }
642
+ : { type: 'double_quote_string', value: obj.left.column },
643
+ ],
644
+ },
645
+ };
646
+ obj.right = {
647
+ type: 'function',
648
+ name: 'LOWER',
649
+ args: {
650
+ type: 'expr_list',
651
+ value: [
652
+ {
653
+ type: 'single_quote_string',
654
+ value: `%${obj.right.value.replaceAll('%', '')}%`,
655
+ },
656
+ ],
657
+ },
658
+ };
659
+ }
660
+ else if (obj.type === 'binary_expr' &&
661
+ (obj.operator === 'NOT IN' || obj.operator === 'IN') &&
662
+ obj.left &&
663
+ (obj.left.type === 'column_ref' ||
664
+ obj.left.type === 'double_quote_string') &&
665
+ obj.right &&
666
+ obj.right.type === 'expr_list' &&
667
+ obj.right.value &&
668
+ obj.right.value.length &&
669
+ obj.right.value[0].type === 'single_quote_string') {
670
+ obj.left = {
671
+ type: 'function',
672
+ name: 'LOWER',
673
+ args: {
674
+ type: 'expr_list',
675
+ value: [
676
+ databaseType === 'BigQuery'
677
+ ? { type: 'column_ref', value: obj.left.column }
678
+ : { type: 'double_quote_string', value: obj.left.column },
679
+ ],
680
+ },
681
+ };
682
+ obj.right = {
683
+ type: 'expr_list',
684
+ // convert NOT IN ('fuel', 'food') to NOT IN (LOWER('fuel'), LOWER('food'))
685
+ value: obj.right.value.map((elem) => ({
686
+ type: 'function',
687
+ name: 'LOWER',
688
+ args: {
689
+ type: 'expr_list',
690
+ value: [{ type: 'single_quote_string', value: elem.value }],
691
+ },
692
+ })),
693
+ };
694
+ }
695
+ else {
696
+ // If the current object does not match the pattern, continue searching its properties
697
+ Object.values(obj).forEach((value) => {
698
+ searchAndReplace(value);
699
+ });
700
+ }
701
+ }
702
+ }
703
+ // Start the recursive search and replace from the root node
704
+ searchAndReplace(node);
705
+ return node;
706
+ }
707
+ // When there is a single "OR" boolean operator, we ensure that there
708
+ // are no parentheses on that object.
709
+ export function convertRemoveSimpleParentheses(ast) {
710
+ const node = ast.where;
711
+ if (isLogicalBinaryExpr(node) &&
712
+ node.operator === 'OR' &&
713
+ !isLogicalBinaryExpr(node.left) &&
714
+ !isLogicalBinaryExpr(node.right) &&
715
+ node.parentheses) {
716
+ delete node.parentheses;
717
+ }
718
+ return { ...ast, where: node };
719
+ }