@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.
- package/dist/cjs/Chart.d.ts +4 -0
- package/dist/cjs/Chart.d.ts.map +1 -1
- package/dist/cjs/Chart.js +5 -5
- package/dist/cjs/ChartBuilder.js +2 -2
- package/dist/cjs/Context.d.ts +1 -1
- package/dist/cjs/Context.d.ts.map +1 -1
- package/dist/cjs/Context.js +3 -1
- package/dist/cjs/Dashboard.d.ts +3 -1
- package/dist/cjs/Dashboard.d.ts.map +1 -1
- package/dist/cjs/Dashboard.js +4 -4
- package/dist/cjs/QuillProvider.d.ts +3 -1
- package/dist/cjs/QuillProvider.d.ts.map +1 -1
- package/dist/cjs/QuillProvider.js +2 -2
- package/dist/cjs/ReportBuilder.d.ts +40 -40
- package/dist/cjs/ReportBuilder.d.ts.map +1 -1
- package/dist/cjs/ReportBuilder.js +2026 -917
- package/dist/cjs/components/Chart/LineChart.d.ts +5 -1
- package/dist/cjs/components/Chart/LineChart.d.ts.map +1 -1
- package/dist/cjs/components/Chart/LineChart.js +18 -6
- package/dist/cjs/components/QuillTable.d.ts +1 -1
- package/dist/cjs/components/QuillTable.d.ts.map +1 -1
- package/dist/cjs/components/QuillTable.js +157 -157
- package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
- package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/AddColumnPopover.js +128 -0
- package/dist/cjs/components/ReportBuilder/ast.d.ts +512 -0
- package/dist/cjs/components/ReportBuilder/ast.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/ast.js +210 -0
- package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts +7 -0
- package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/bigDateMap.js +689 -0
- package/dist/cjs/components/ReportBuilder/constants.d.ts +89 -0
- package/dist/cjs/components/ReportBuilder/constants.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/constants.js +130 -0
- package/dist/cjs/components/ReportBuilder/convert.d.ts +41 -0
- package/dist/cjs/components/ReportBuilder/convert.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/convert.js +730 -0
- package/dist/cjs/components/ReportBuilder/operators.d.ts +445 -0
- package/dist/cjs/components/ReportBuilder/operators.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/operators.js +552 -0
- package/dist/cjs/components/ReportBuilder/pivot.d.ts +10 -0
- package/dist/cjs/components/ReportBuilder/pivot.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/pivot.js +2 -0
- package/dist/cjs/components/ReportBuilder/postgres.d.ts +150 -0
- package/dist/cjs/components/ReportBuilder/postgres.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/postgres.js +365 -0
- package/dist/cjs/components/ReportBuilder/schema.d.ts +23 -0
- package/dist/cjs/components/ReportBuilder/schema.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/schema.js +2 -0
- package/dist/cjs/components/ReportBuilder/ui.d.ts +34 -0
- package/dist/cjs/components/ReportBuilder/ui.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/ui.js +389 -0
- package/dist/cjs/components/ReportBuilder/util.d.ts +76 -0
- package/dist/cjs/components/ReportBuilder/util.d.ts.map +1 -0
- package/dist/cjs/components/ReportBuilder/util.js +648 -0
- package/dist/cjs/components/UiComponents.d.ts +15 -2
- package/dist/cjs/components/UiComponents.d.ts.map +1 -1
- package/dist/cjs/components/UiComponents.js +50 -3
- package/dist/cjs/utils/crypto.d.ts +1 -1
- package/dist/cjs/utils/crypto.d.ts.map +1 -1
- package/dist/cjs/utils/crypto.js +9 -5
- package/dist/esm/Chart.d.ts +4 -0
- package/dist/esm/Chart.d.ts.map +1 -1
- package/dist/esm/Chart.js +5 -5
- package/dist/esm/ChartBuilder.js +1 -1
- package/dist/esm/Context.d.ts +1 -1
- package/dist/esm/Context.d.ts.map +1 -1
- package/dist/esm/Context.js +3 -1
- package/dist/esm/Dashboard.d.ts +3 -1
- package/dist/esm/Dashboard.d.ts.map +1 -1
- package/dist/esm/Dashboard.js +4 -4
- package/dist/esm/QuillProvider.d.ts +3 -1
- package/dist/esm/QuillProvider.d.ts.map +1 -1
- package/dist/esm/QuillProvider.js +2 -2
- package/dist/esm/ReportBuilder.d.ts +40 -40
- package/dist/esm/ReportBuilder.d.ts.map +1 -1
- package/dist/esm/ReportBuilder.js +2028 -917
- package/dist/esm/components/Chart/LineChart.d.ts +5 -1
- package/dist/esm/components/Chart/LineChart.d.ts.map +1 -1
- package/dist/esm/components/Chart/LineChart.js +18 -6
- package/dist/esm/components/QuillTable.d.ts +1 -1
- package/dist/esm/components/QuillTable.d.ts.map +1 -1
- package/dist/esm/components/QuillTable.js +157 -157
- package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
- package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/AddColumnPopover.js +125 -0
- package/dist/esm/components/ReportBuilder/ast.d.ts +512 -0
- package/dist/esm/components/ReportBuilder/ast.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/ast.js +186 -0
- package/dist/esm/components/ReportBuilder/bigDateMap.d.ts +7 -0
- package/dist/esm/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/bigDateMap.js +686 -0
- package/dist/esm/components/ReportBuilder/constants.d.ts +89 -0
- package/dist/esm/components/ReportBuilder/constants.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/constants.js +127 -0
- package/dist/esm/components/ReportBuilder/convert.d.ts +41 -0
- package/dist/esm/components/ReportBuilder/convert.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/convert.js +719 -0
- package/dist/esm/components/ReportBuilder/operators.d.ts +445 -0
- package/dist/esm/components/ReportBuilder/operators.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/operators.js +548 -0
- package/dist/esm/components/ReportBuilder/pivot.d.ts +10 -0
- package/dist/esm/components/ReportBuilder/pivot.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/pivot.js +1 -0
- package/dist/esm/components/ReportBuilder/postgres.d.ts +150 -0
- package/dist/esm/components/ReportBuilder/postgres.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/postgres.js +355 -0
- package/dist/esm/components/ReportBuilder/schema.d.ts +23 -0
- package/dist/esm/components/ReportBuilder/schema.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/schema.js +1 -0
- package/dist/esm/components/ReportBuilder/ui.d.ts +34 -0
- package/dist/esm/components/ReportBuilder/ui.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/ui.js +366 -0
- package/dist/esm/components/ReportBuilder/util.d.ts +76 -0
- package/dist/esm/components/ReportBuilder/util.d.ts.map +1 -0
- package/dist/esm/components/ReportBuilder/util.js +616 -0
- package/dist/esm/components/UiComponents.d.ts +15 -2
- package/dist/esm/components/UiComponents.d.ts.map +1 -1
- package/dist/esm/components/UiComponents.js +47 -2
- package/dist/esm/utils/crypto.d.ts +1 -1
- package/dist/esm/utils/crypto.d.ts.map +1 -1
- package/dist/esm/utils/crypto.js +9 -5
- 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
|
+
}
|