• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

demmings / gsSQL / 3920713201

pending completion
3920713201

push

github

cdemmigs
JSDoc updates

1183 of 1254 branches covered (94.34%)

Branch coverage included in aggregate %.

807 of 807 new or added lines in 7 files covered. (100.0%)

9211 of 9623 relevant lines covered (95.72%)

490.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.48
/src/Views.js
1
//  Remove comments for testing in NODE
1✔
2
/*  *** DEBUG START ***
1✔
3
export { DERIVEDTABLE, VirtualFields, VirtualField, SelectTables };
1✔
4
import { Table } from './Table.js';
1✔
5
import { Sql } from './Sql.js';
1✔
6
import { SqlParse } from './SimpleParser.js';
1✔
7
//  *** DEBUG END  ***/
1✔
8

1✔
9
const DERIVEDTABLE = "::DERIVEDTABLE::";
1✔
10

1✔
11
/** Perform SQL SELECT operations to retrieve requested data. */
1✔
12
class SelectTables {
1✔
13
    /**
1✔
14
     * @param {Object} ast - Abstract Syntax Tree
1✔
15
     * @param {Map<String,Table>} tableInfo - Map of table info.
1✔
16
     * @param {any[]} bindVariables - List of bind data.
1✔
17
     */
1✔
18
    constructor(ast, tableInfo, bindVariables) {
1✔
19
        /** @property {String} - primary table name. */
206✔
20
        this.primaryTable = ast.FROM[0].table;
206✔
21

206✔
22
        /** @property {Object} - AST of SELECT fields */
206✔
23
        this.astFields = ast.SELECT;
206✔
24

206✔
25
        /** @property {Map<String,Table>} tableInfo - Map of table info. */
206✔
26
        this.tableInfo = tableInfo;
206✔
27

206✔
28
        /** @property {any[]} - Bind variable data. */
206✔
29
        this.bindVariables = bindVariables;
206✔
30

206✔
31
        /** @property {JoinTables} - Join table object. */
206✔
32
        this.dataJoin = new JoinTables([]);
206✔
33

206✔
34
        /** @property {TableFields} */
206✔
35
        this.tableFields = new TableFields();
206✔
36

206✔
37
        if (!tableInfo.has(this.primaryTable.toUpperCase()))
206✔
38
            throw new Error(`Invalid table name: ${this.primaryTable}`);
206✔
39

205✔
40
        /** @property {Table} - Primary table info. */
205✔
41
        this.primaryTableInfo = tableInfo.get(this.primaryTable.toUpperCase());
205✔
42

205✔
43
        //  Keep a list of all possible fields from all tables.
205✔
44
        this.tableFields.loadVirtualFields(this.primaryTable, tableInfo);
205✔
45

205✔
46
        //  Expand any 'SELECT *' fields and add the actual field names into 'astFields'.
205✔
47
        this.astFields = VirtualFields.expandWildcardFields(this.primaryTableInfo, this.astFields);
205✔
48

205✔
49
        //  Define the data source of each field in SELECT field list.
205✔
50
        this.tableFields.updateSelectFieldList(this.astFields);
205✔
51

205✔
52
        //  These are fields REFERENCED, but not actually in the SELECT FIELDS.
205✔
53
        //  So columns referenced by GROUP BY, ORDER BY and not in SELECT.
205✔
54
        //  These temp columns need to be removed after processing.
205✔
55
        this.tableFields.addReferencedColumnstoSelectFieldList(ast);
205✔
56
    }
206✔
57

1✔
58
    /**
1✔
59
     * Process any JOIN condition.
1✔
60
     * @param {Object} ast - Abstract Syntax Tree
1✔
61
     */
1✔
62
    join(ast) {
1✔
63
        if (typeof ast.JOIN !== 'undefined')
204✔
64
            this.dataJoin = new JoinTables(ast.JOIN, this.tableFields);
204✔
65
    }
204✔
66

1✔
67
    /**
1✔
68
      * Retrieve filtered record ID's.
1✔
69
      * @param {Object} ast - Abstract Syntax Tree
1✔
70
      * @returns {Number[]} - Records ID's that match WHERE condition.
1✔
71
      */
1✔
72
    whereCondition(ast) {
1✔
73
        let sqlData = [];
201✔
74

201✔
75
        let conditions = {};
201✔
76
        if (typeof ast.WHERE !== 'undefined') {
201✔
77
            conditions = ast.WHERE;
110✔
78
        }
110✔
79
        else {
91✔
80
            //  Entire table is selected.  
91✔
81
            conditions = { operator: "=", left: "\"A\"", right: "\"A\"" };
91✔
82
        }
91✔
83

201✔
84
        if (typeof conditions.logic === 'undefined')
201✔
85
            sqlData = this.resolveCondition("OR", [conditions]);
201✔
86
        else
22✔
87
            sqlData = this.resolveCondition(conditions.logic, conditions.terms);
22✔
88

193✔
89
        return sqlData;
193✔
90
    }
201✔
91

1✔
92
    /**
1✔
93
    * Recursively resolve WHERE condition and then apply AND/OR logic to results.
1✔
94
    * @param {String} logic - logic condition (AND/OR) between terms
1✔
95
    * @param {Object} terms - terms of WHERE condition (value compared to value)
1✔
96
    * @returns {Number[]} - record ID's 
1✔
97
    */
1✔
98
    resolveCondition(logic, terms) {
1✔
99
        const recordIDs = [];
207✔
100

207✔
101
        for (const cond of terms) {
207✔
102
            if (typeof cond.logic === 'undefined') {
239✔
103
                recordIDs.push(this.getRecordIDs(cond));
233✔
104
            }
233✔
105
            else {
6✔
106
                recordIDs.push(this.resolveCondition(cond.logic, cond.terms));
6✔
107
            }
6✔
108
        }
239✔
109

199✔
110
        let result = [];
199✔
111
        if (logic === "AND") {
207✔
112
            result = recordIDs.reduce((a, b) => a.filter(c => b.includes(c)));
20✔
113
        }
20✔
114
        if (logic === "OR") {
207✔
115
            //  OR Logic
179✔
116
            let tempArr = [];
179✔
117
            for (const arr of recordIDs) {
179✔
118
                tempArr = tempArr.concat(arr);
188✔
119
            }
188✔
120
            result = Array.from(new Set(tempArr));
179✔
121
        }
179✔
122

199✔
123
        return result;
199✔
124
    }
207✔
125

1✔
126
    /**
1✔
127
    * Find record ID's where condition is TRUE.
1✔
128
    * @param {Object} condition - WHERE test condition
1✔
129
    * @returns {Number[]} - record ID's which are true.
1✔
130
    */
1✔
131
    getRecordIDs(condition) {
1✔
132
        /** @type {Number[]} */
233✔
133
        const recordIDs = [];
233✔
134

233✔
135
        const leftFieldConditions = this.resolveFieldCondition(condition.left);
233✔
136
        const rightFieldConditions = this.resolveFieldCondition(condition.right);
233✔
137

233✔
138
        /** @type {Table} */
233✔
139
        this.masterTable = this.dataJoin.isDerivedTable() ? this.dataJoin.getJoinedTableInfo() : this.primaryTableInfo;
233✔
140
        const calcSqlField = new CalculatedField(this.masterTable, this.primaryTableInfo, this.tableFields);
233✔
141

233✔
142
        for (let masterRecordID = 1; masterRecordID < this.masterTable.tableData.length; masterRecordID++) {
233✔
143
            let leftValue = SelectTables.getConditionValue(leftFieldConditions, calcSqlField, masterRecordID);
2,236✔
144
            let rightValue = SelectTables.getConditionValue(rightFieldConditions, calcSqlField, masterRecordID);
2,236✔
145

2,236✔
146
            if (leftValue instanceof Date || rightValue instanceof Date) {
2,236✔
147
                leftValue = SelectTables.dateToMs(leftValue);
346✔
148
                rightValue = SelectTables.dateToMs(rightValue);
346✔
149
            }
346✔
150

2,230✔
151
            if (SelectTables.isConditionTrue(leftValue, condition.operator, rightValue))
2,230✔
152
                recordIDs.push(masterRecordID);
2,236✔
153
        }
2,236✔
154

225✔
155
        return recordIDs;
225✔
156
    }
233✔
157

1✔
158
    /**
1✔
159
     * Evaulate value on left/right side of condition
1✔
160
     * @param {ResolvedFieldCondition} fieldConditions - the value to be found will come from:
1✔
161
     * * constant data
1✔
162
     * * field data
1✔
163
     * * calculated field
1✔
164
     * * sub-query 
1✔
165
     * @param {CalculatedField} calcSqlField - data to resolve the calculated field.
1✔
166
     * @param {Number} masterRecordID - current record in table to grab field data from
1✔
167
     * @returns {any} - resolve value.
1✔
168
     */
1✔
169
    static getConditionValue(fieldConditions, calcSqlField, masterRecordID) {
1✔
170
        let leftValue = fieldConditions.constantData;
4,470✔
171
        if (fieldConditions.columnNumber >= 0) {
4,470✔
172
            leftValue = fieldConditions.fieldConditionTableInfo.tableData[masterRecordID][fieldConditions.columnNumber];
1,370✔
173
        }
1,370✔
174
        else if (fieldConditions.calculatedField !== "") {
3,100✔
175
            if (fieldConditions.calculatedField.toUpperCase() === "NULL") {
80✔
176
                leftValue = "NULL";
10✔
177
            }
10✔
178
            else {
70✔
179
                leftValue = calcSqlField.evaluateCalculatedField(fieldConditions.calculatedField, masterRecordID);
70✔
180
            }
70✔
181
        }
80✔
182
        else if (fieldConditions.subQuery !== null) {
3,020✔
183
            const arrayResult = fieldConditions.subQuery.select(masterRecordID, calcSqlField);
23✔
184
            if (typeof arrayResult !== 'undefined' && arrayResult !== null && arrayResult.length > 0)
23✔
185
                leftValue = arrayResult[0][0];
23✔
186
        }
23✔
187

4,464✔
188
        return leftValue;
4,464✔
189
    }
4,470✔
190

1✔
191
    /**
1✔
192
     * Compare where term values using operator and see if comparision is true.
1✔
193
     * @param {any} leftValue - left value of condition
1✔
194
     * @param {String} operator - operator for comparision
1✔
195
     * @param {any} rightValue  - right value of condition
1✔
196
     * @returns {Boolean} - is comparison true.
1✔
197
     */
1✔
198
    static isConditionTrue(leftValue, operator, rightValue) {
1✔
199
        let keep = false;
2,230✔
200

2,230✔
201
        switch (operator.toUpperCase()) {
2,230✔
202
            case "=":
2,230✔
203
                keep = leftValue == rightValue;         // skipcq: JS-0050
1,244✔
204
                break;
1,244✔
205

2,230✔
206
            case ">":
2,230✔
207
                keep = leftValue > rightValue;
149✔
208
                break;
149✔
209

2,230✔
210
            case "<":
2,230✔
211
                keep = leftValue < rightValue;
60✔
212
                break;
60✔
213

2,230✔
214
            case ">=":
2,230✔
215
                keep = leftValue >= rightValue;
228✔
216
                break;
228✔
217

2,230✔
218
            case "<=":
2,230✔
219
                keep = leftValue <= rightValue;
228✔
220
                break;
228✔
221

2,230✔
222
            case "<>":
2,230✔
223
                keep = leftValue != rightValue;         // skipcq: JS-0050
30✔
224
                break;
30✔
225

2,230✔
226
            case "!=":
2,230✔
227
                keep = leftValue != rightValue;         // skipcq: JS-0050
30✔
228
                break;
30✔
229

2,230✔
230
            case "LIKE":
2,230✔
231
                keep = SelectTables.likeCondition(leftValue, rightValue);
61✔
232
                break;
61✔
233

2,230✔
234
            case "NOT LIKE":
2,230✔
235
                keep = !(SelectTables.likeCondition(leftValue, rightValue));
10✔
236
                break;
10✔
237

2,230✔
238
            case "IN":
2,230✔
239
                keep = SelectTables.inCondition(leftValue, rightValue);
156✔
240
                break;
156✔
241

2,230✔
242
            case "NOT IN":
2,230✔
243
                keep = !(SelectTables.inCondition(leftValue, rightValue));
9✔
244
                break;
9✔
245

2,230✔
246
            case "IS NOT":
2,230✔
247
                keep = !(SelectTables.isCondition(leftValue, rightValue));
5✔
248
                break;
5✔
249

2,230✔
250
            case "IS":
2,230✔
251
                keep = SelectTables.isCondition(leftValue, rightValue);
5✔
252
                break;
5✔
253

2,230✔
254
            case "EXISTS":
2,230✔
255
                keep = SelectTables.existsCondition(rightValue);
7✔
256
                break;
7✔
257

2,230✔
258
            case "NOT EXISTS":
2,230✔
259
                keep = !(SelectTables.existsCondition(rightValue));
7✔
260
                break;
7✔
261

2,230✔
262
            default:
2,230✔
263
                throw new Error(`Invalid Operator: ${operator}`);
1✔
264
        }
2,230✔
265

2,229✔
266
        return keep;
2,229✔
267
    }
2,230✔
268

1✔
269
    /**
1✔
270
     * Retrieve the data for the record ID's specified for ALL SELECT fields.
1✔
271
     * @param {Number[]} recordIDs - record ID's which are SELECTed.
1✔
272
     * @returns {any[][]} - double array of select data.  No column title is included here.
1✔
273
     */
1✔
274
    getViewData(recordIDs) {
1✔
275
        const virtualData = [];
193✔
276
        const calcSqlField = new CalculatedField(this.masterTable, this.primaryTableInfo, this.tableFields);
193✔
277
        const subQuery = new CorrelatedSubQuery(this.tableInfo, this.tableFields);
193✔
278

193✔
279
        for (const masterRecordID of recordIDs) {
193✔
280
            const newRow = [];
1,215✔
281

1,215✔
282
            for (const field of this.tableFields.getSelectFields()) {
1,215✔
283
                if (field.tableInfo !== null)
5,451✔
284
                    newRow.push(field.getData(masterRecordID));
5,451✔
285
                else if (field.subQueryAst !== null) {
1,037✔
286
                    const result = subQuery.select(masterRecordID, calcSqlField, field.subQueryAst);
11✔
287
                    newRow.push(result[0][0]);
11✔
288
                }
11✔
289
                else if (field.calculatedFormula !== "") {
1,026✔
290
                    const result = calcSqlField.evaluateCalculatedField(field.calculatedFormula, masterRecordID);
1,026✔
291
                    newRow.push(result);
1,026✔
292
                }
1,026✔
293
            }
5,451✔
294

1,209✔
295
            virtualData.push(newRow);
1,209✔
296
        }
1,209✔
297

187✔
298
        return virtualData;
187✔
299
    }
193✔
300

1✔
301
    /**
1✔
302
     * Returns the entire string in UPPER CASE - except for anything between quotes.
1✔
303
     * @param {String} srcString - source string to convert.
1✔
304
     * @returns {String} - converted string.
1✔
305
     */
1✔
306
    static toUpperCaseExceptQuoted(srcString) {
1✔
307
        let finalString = "";
124✔
308
        let inQuotes = "";
124✔
309

124✔
310
        for (let i = 0; i < srcString.length; i++) {
124✔
311
            let ch = srcString.charAt(i);
4,705✔
312

4,705✔
313
            if (inQuotes === "") {
4,705✔
314
                if (ch === '"' || ch === "'")
4,129✔
315
                    inQuotes = ch;
4,129✔
316
                ch = ch.toUpperCase();
4,129✔
317
            }
4,129✔
318
            else {
576✔
319
                if (ch === inQuotes)
576✔
320
                    inQuotes = "";
576✔
321
            }
576✔
322

4,705✔
323
            finalString += ch;
4,705✔
324
        }
4,705✔
325

124✔
326
        return finalString;
124✔
327
    }
124✔
328

1✔
329
    /**
1✔
330
     * Parse input string for 'func' and then parse if found.
1✔
331
     * @param {String} functionString - Select field which may contain a function.
1✔
332
     * @param {String} func - Function name to parse for.
1✔
333
     * @returns {String[]} - Parsed function string.
1✔
334
     *   * null if function not found, 
1✔
335
     *   * string array[0] - original string, e.g. **sum(quantity)**
1✔
336
     *   * string array[1] - function parameter, e.g. **quantity**
1✔
337
     */
1✔
338
    static parseForFunctions(functionString, func) {
1✔
339
        const args = [];
4,155✔
340
        const expMatch = "%1\\s*\\(";
4,155✔
341

4,155✔
342
        const matchStr = new RegExp(expMatch.replace("%1", func));
4,155✔
343
        const startMatchPos = functionString.search(matchStr);
4,155✔
344
        if (startMatchPos !== -1) {
4,155✔
345
            const searchStr = functionString.substring(startMatchPos);
179✔
346
            let i = searchStr.indexOf("(");
179✔
347
            const startLeft = i;
179✔
348
            let leftBracket = 1;
179✔
349
            for (i = i + 1; i < searchStr.length; i++) {
179✔
350
                const ch = searchStr.charAt(i);
4,657✔
351
                if (ch === "(") leftBracket++;
4,657✔
352
                if (ch === ")") leftBracket--;
4,657✔
353

4,657✔
354
                if (leftBracket === 0) {
4,657✔
355
                    args.push(searchStr.substring(0, i + 1));
179✔
356
                    args.push(searchStr.substring(startLeft + 1, i));
179✔
357
                    return args;
179✔
358
                }
179✔
359
            }
4,657✔
360
        }
179✔
361

3,976✔
362
        return null;
3,976✔
363
    }
4,155✔
364

1✔
365
    /**
1✔
366
     * Parse the input for a calculated field.
1✔
367
     * String split on comma, EXCEPT if comma is within brackets (i.e. within an inner function)
1✔
368
     * @param {String} paramString - Search and parse this string for parameters.
1✔
369
     * @returns {String[]} - List of function parameters.
1✔
370
     */
1✔
371
    static parseForParams(paramString, startBracket = "(", endBracket = ")") {
1✔
372
        const args = [];
106✔
373
        let bracketCount = 0;
106✔
374
        let start = 0;
106✔
375

106✔
376
        for (let i = 0; i < paramString.length; i++) {
106✔
377
            const ch = paramString.charAt(i);
1,936✔
378

1,936✔
379
            if (ch === "," && bracketCount === 0) {
1,936✔
380
                args.push(paramString.substring(start, i));
60✔
381
                start = i + 1;
60✔
382
            }
60✔
383
            else if (ch === startBracket)
1,876✔
384
                bracketCount++;
1,876✔
385
            else if (ch === endBracket)
1,865✔
386
                bracketCount--;
1,865✔
387
        }
1,936✔
388

106✔
389
        const lastStr = paramString.substring(start);
106✔
390
        if (lastStr !== "")
106✔
391
            args.push(lastStr);
106✔
392

106✔
393
        return args;
106✔
394
    }
106✔
395

1✔
396
    /**
1✔
397
     * Compress the table data so there is one record per group (fields in GROUP BY).
1✔
398
     * The other fields MUST be aggregate calculated fields that works on the data in that group.
1✔
399
     * @param {Object} ast - Abstract Syntax Tree
1✔
400
     * @param {any[][]} viewTableData - Table data.
1✔
401
     * @returns {any[][]} - Aggregated table data.
1✔
402
     */
1✔
403
    groupBy(ast, viewTableData) {
1✔
404
        let groupedTableData = viewTableData;
187✔
405

187✔
406
        if (typeof ast['GROUP BY'] !== 'undefined') {
187✔
407
            groupedTableData = this.groupByFields(ast['GROUP BY'], viewTableData);
25✔
408

25✔
409
            if (typeof ast.HAVING !== 'undefined') {
25✔
410
                groupedTableData = this.having(ast.HAVING, groupedTableData);
2✔
411
            }
2✔
412
        }
25✔
413
        else {
162✔
414
            //  If any conglomerate field functions (SUM, COUNT,...)
162✔
415
            //  we summarize all records into ONE.
162✔
416
            if (this.tableFields.getConglomerateFieldCount() > 0) {
162✔
417
                const compressedData = [];
31✔
418
                const conglomerate = new ConglomerateRecord(this.tableFields.getSelectFields());
31✔
419
                compressedData.push(conglomerate.squish(viewTableData));
31✔
420
                groupedTableData = compressedData;
31✔
421
            }
31✔
422
        }
162✔
423

187✔
424
        return groupedTableData;
187✔
425
    }
187✔
426

1✔
427
    /**
1✔
428
    * Group table data by group fields.
1✔
429
    * @param {any[]} astGroupBy - AST group by fields.
1✔
430
    * @param {any[][]} selectedData - table data
1✔
431
    * @returns {any[][]} - compressed table data
1✔
432
    */
1✔
433
    groupByFields(astGroupBy, selectedData) {
1✔
434
        if (selectedData.length === 0)
25✔
435
            return selectedData;
25✔
436

24✔
437
        //  Sort the least important first, and most important last.
24✔
438
        astGroupBy.reverse();
24✔
439

24✔
440
        for (const orderField of astGroupBy) {
25✔
441
            const selectColumn = this.tableFields.getSelectFieldColumn(orderField.column);
27✔
442
            if (selectColumn !== -1) {
27✔
443
                SelectTables.sortByColumnASC(selectedData, selectColumn);
27✔
444
            }
27✔
445
        }
27✔
446

24✔
447
        const groupedData = [];
24✔
448
        let groupRecords = [];
24✔
449
        const conglomerate = new ConglomerateRecord(this.tableFields.getSelectFields());
24✔
450

24✔
451
        let lastKey = this.createGroupByKey(selectedData[0], astGroupBy);
24✔
452
        for (const row of selectedData) {
25✔
453
            const newKey = this.createGroupByKey(row, astGroupBy);
231✔
454
            if (newKey !== lastKey) {
231✔
455
                groupedData.push(conglomerate.squish(groupRecords));
95✔
456

95✔
457
                lastKey = newKey;
95✔
458
                groupRecords = [];
95✔
459
            }
95✔
460
            groupRecords.push(row);
231✔
461
        }
231✔
462

24✔
463
        if (groupRecords.length > 0)
24✔
464
            groupedData.push(conglomerate.squish(groupRecords));
24✔
465

24✔
466
        return groupedData;
24✔
467
    }
25✔
468

1✔
469
    /**
1✔
470
     * Create a composite key that is comprised from all field data in group by clause.
1✔
471
     * @param {any[]} row  - current row of data.
1✔
472
     * @param {any[]} astGroupBy - group by fields
1✔
473
     * @returns {String} - group key
1✔
474
     */
1✔
475
    createGroupByKey(row, astGroupBy) {
1✔
476
        let key = "";
255✔
477

255✔
478
        for (const orderField of astGroupBy) {
255✔
479
            const selectColumn = this.tableFields.getSelectFieldColumn(orderField.column);
288✔
480
            if (selectColumn !== -1)
288✔
481
                key += row[selectColumn].toString();
288✔
482
        }
288✔
483

255✔
484
        return key;
255✔
485
    }
255✔
486

1✔
487
    /**
1✔
488
    * Take the compressed data from GROUP BY and then filter those records using HAVING conditions.
1✔
489
    * @param {Object} astHaving - AST HAVING conditons
1✔
490
    * @param {any[][]} selectedData - compressed table data (from group by)
1✔
491
    * @returns {any[][]} - filtered data using HAVING conditions.
1✔
492
    */
1✔
493
    having(astHaving, selectedData) {
1✔
494
        //  Add in the title row for now
2✔
495
        selectedData.unshift(this.tableFields.getColumnNames());
2✔
496

2✔
497
        //  Create our virtual GROUP table with data already selected.
2✔
498
        const groupTable = new Table(this.primaryTable).loadArrayData(selectedData);
2✔
499

2✔
500
        const tableMapping = new Map();
2✔
501
        tableMapping.set(this.primaryTable.toUpperCase(), groupTable);
2✔
502

2✔
503
        //  Set up for our SQL.
2✔
504
        const inSQL = new Sql().setTables(tableMapping);
2✔
505

2✔
506
        //  Fudge the HAVING to look like a SELECT.
2✔
507
        const astSelect = {};
2✔
508
        astSelect.FROM = [{ table: this.primaryTable }];
2✔
509
        astSelect.SELECT = [{ name: "*" }];
2✔
510
        astSelect.WHERE = astHaving;
2✔
511

2✔
512
        return inSQL.select(astSelect);
2✔
513
    }
2✔
514

1✔
515
    /**
1✔
516
     * Take select data and sort by columns specified in ORDER BY clause.
1✔
517
     * @param {Object} ast - Abstract Syntax Tree for SELECT
1✔
518
     * @param {any[][]} selectedData - Table data to sort.  On function return, this array is sorted.
1✔
519
     */
1✔
520
    orderBy(ast, selectedData) {
1✔
521
        if (typeof ast['ORDER BY'] === 'undefined')
187✔
522
            return;
187✔
523

22✔
524
        const astOrderby = ast['ORDER BY']
22✔
525

22✔
526
        //  Sort the least important first, and most important last.
22✔
527
        const reverseOrderBy = astOrderby.reverse();
22✔
528

22✔
529
        for (const orderField of reverseOrderBy) {
187✔
530
            const selectColumn = this.tableFields.getSelectFieldColumn(orderField.column);
23✔
531

23✔
532
            if (selectColumn === -1) {
23!
533
                throw new Error(`Invalid ORDER BY: ${orderField.column}`);
×
534
            }
×
535

23✔
536
            if (orderField.order.toUpperCase() === "DESC") {
23✔
537
                SelectTables.sortByColumnDESC(selectedData, selectColumn);
3✔
538
            }
3✔
539
            else {
20✔
540
                SelectTables.sortByColumnASC(selectedData, selectColumn);
20✔
541
            }
20✔
542
        }
23✔
543
    }
187✔
544

1✔
545
    /**
1✔
546
     * Removes temporary fields from return data.  These temporary fields were needed to generate
1✔
547
     * the final table data, but are not included in the SELECT fields for final output.
1✔
548
     * @param {any[][]} viewTableData - table data that may contain temporary columns.
1✔
549
     * @returns {any[][]} - table data with temporary columns removed.
1✔
550
     */
1✔
551
    removeTempColumns(viewTableData) {
1✔
552
        const tempColumns = this.tableFields.getTempSelectedColumnNumbers();
187✔
553

187✔
554
        if (tempColumns.length === 0)
187✔
555
            return viewTableData;
187✔
556

3✔
557
        for (const row of viewTableData) {
187✔
558
            for (const col of tempColumns) {
22✔
559
                row.splice(col, 1);
29✔
560
            }
29✔
561
        }
22✔
562

3✔
563
        return viewTableData;
3✔
564
    }
187✔
565

1✔
566
    /**
1✔
567
     * Sort the table data from lowest to highest using the data in colIndex for sorting.
1✔
568
     * @param {any[][]} tableData - table data to sort.
1✔
569
     * @param {Number} colIndex - column index which indicates which column to use for sorting.
1✔
570
     * @returns {any[][]} - sorted table data.
1✔
571
     */
1✔
572
    static sortByColumnASC(tableData, colIndex) {
1✔
573
        tableData.sort(sortFunction);
47✔
574

47✔
575
        /**
47✔
576
         * 
47✔
577
         * @param {any} a 
47✔
578
         * @param {any} b 
47✔
579
         * @returns {Number}
47✔
580
         */
47✔
581
        function sortFunction(a, b) {
47✔
582
            if (a[colIndex] === b[colIndex]) {
600✔
583
                return 0;
162✔
584
            }
162✔
585
            return (a[colIndex] < b[colIndex]) ? -1 : 1;
600✔
586
        }
600✔
587

47✔
588
        return tableData;
47✔
589
    }
47✔
590

1✔
591
    /**
1✔
592
     * Sort the table data from highest to lowest using the data in colIndex for sorting.
1✔
593
     * @param {any[][]} tableData - table data to sort.
1✔
594
     * @param {Number} colIndex - column index which indicates which column to use for sorting.
1✔
595
     * @returns {any[][]} - sorted table data.
1✔
596
     */
1✔
597
    static sortByColumnDESC(tableData, colIndex) {
1✔
598

3✔
599
        tableData.sort(sortFunction);
3✔
600

3✔
601
        /**
3✔
602
         * 
3✔
603
         * @param {any} a 
3✔
604
         * @param {any} b 
3✔
605
         * @returns {Number}
3✔
606
         */
3✔
607
        function sortFunction(a, b) {
3✔
608
            if (a[colIndex] === b[colIndex]) {
58✔
609
                return 0;
13✔
610
            }
13✔
611
            return (a[colIndex] > b[colIndex]) ? -1 : 1;
58✔
612
        }
58✔
613

3✔
614
        return tableData;
3✔
615
    }
3✔
616

1✔
617
    /**
1✔
618
     * @typedef {Object} ResolvedFieldCondition
1✔
619
     * @property {Table} fieldConditionTableInfo
1✔
620
     * @property {Number} columnNumber - use column data from this column, unless -1.
1✔
621
     * @property {String} constantData - constant data used for column, unless null.
1✔
622
     * @property {String} calculatedField - calculation of data for column, unless empty.
1✔
623
     * @property {CorrelatedSubQuery} subQuery - use this correlated subquery object if not null. 
1✔
624
     * 
1✔
625
     */
1✔
626
    /**
1✔
627
     * Determine what the source of value is for the current field condition.
1✔
628
     * @param {Object} fieldCondition - left or right portion of condition
1✔
629
     * @returns {ResolvedFieldCondition}
1✔
630
     */
1✔
631
    resolveFieldCondition(fieldCondition) {
1✔
632
        /** @type {String} */
466✔
633
        let constantData = null;
466✔
634
        /** @type {Number} */
466✔
635
        let columnNumber = -1;
466✔
636
        /** @type {Table} */
466✔
637
        let fieldConditionTableInfo = null;
466✔
638
        /** @type {String} */
466✔
639
        let calculatedField = "";
466✔
640
        /** @type {CorrelatedSubQuery} */
466✔
641
        let subQuery = null;
466✔
642

466✔
643
        //  Maybe a SELECT within...
466✔
644
        if (typeof fieldCondition.SELECT !== 'undefined') {
466✔
645
            const subQueryTableInfo = SelectTables.getSubQueryTableSet(fieldCondition, this.tableInfo);
22✔
646

22✔
647
            const inSQL = new Sql().setTables(subQueryTableInfo);
22✔
648
            inSQL.setBindValues(this.bindVariables);
22✔
649
            let inData = null;
22✔
650
            try {
22✔
651
                inData = inSQL.select(fieldCondition);
22✔
652
                constantData = inData.join(",");
22✔
653
            }
22✔
654
            catch (ex) {
22✔
655
                // IF (big if) a correlated sub-query, it will fail trying to reference a field
3✔
656
                // from the outer query.
3✔
657
                subQuery = new CorrelatedSubQuery(this.tableInfo, this.tableFields, fieldCondition);
3✔
658
            }
3✔
659
        }
22✔
660
        else if (SelectTables.isStringConstant(fieldCondition))
444✔
661
            constantData = SelectTables.extractStringConstant(fieldCondition);
444✔
662
        else if (fieldCondition === '?') {
239✔
663
            //  Bind variable data.
84✔
664
            if (this.bindVariables.length === 0)
84✔
665
                throw new Error("Bind variable mismatch");
84✔
666
            constantData = this.bindVariables.shift();
83✔
667
        }
83✔
668
        else {
155✔
669
            if (isNaN(fieldCondition)) {
155✔
670
                if (this.tableFields.hasField(fieldCondition)) {
147✔
671
                    columnNumber = this.tableFields.getFieldColumn(fieldCondition)
132✔
672
                    fieldConditionTableInfo = this.tableFields.getTableInfo(fieldCondition)
132✔
673
                }
132✔
674
                else {
15✔
675
                    //  Calculated field?
15✔
676
                    calculatedField = fieldCondition;
15✔
677
                }
15✔
678
            }
147✔
679
            else
8✔
680
                constantData = fieldCondition;
8✔
681
        }
155✔
682

465✔
683
        return { fieldConditionTableInfo, columnNumber, constantData, calculatedField, subQuery };
465✔
684
    }
466✔
685

1✔
686
    /**
1✔
687
     * Create a set of tables that are used in sub-query.
1✔
688
     * @param {Object} ast - Sub-query AST.
1✔
689
     * @param {Map<String,Table>} tableInfo - Master set of tables used for entire select.
1✔
690
     * @returns {Map<String,Table>} - table set for sub-query.
1✔
691
     */
1✔
692
    static getSubQueryTableSet(ast, tableInfo) {
1✔
693
        const tableSubSet = new Map();
22✔
694
        const selectTables = Sql.getReferencedTableNamesFromAst(ast);
22✔
695

22✔
696
        for (const found of selectTables) {
22✔
697
            if (found[0] !== "" && !tableSubSet.has(found[0])) {
23✔
698
                tableSubSet.set(found[0], tableInfo.get(found[0]));
23✔
699
            }
23✔
700
            if (found[1] !== "" && !tableSubSet.has(found[1])) {
23!
701
                tableSubSet.set(found[1], tableInfo.get(found[1]));
×
702
            }
×
703
        }
23✔
704

22✔
705
        return tableSubSet;
22✔
706
    }
22✔
707

1✔
708
    /**
1✔
709
     * Is the string a constant in the SELECT condition.  
1✔
710
     * @param {String} value - condition to test
1✔
711
     * @returns {Boolean} - Is this string a constant.
1✔
712
     */
1✔
713
    static isStringConstant(value) {
1✔
714
        return value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'");
444✔
715
    }
444✔
716

1✔
717
    /**
1✔
718
     * Extract the string literal out of condition.  This removes surrounding quotes.
1✔
719
     * @param {String} value - String that encloses literal string data.
1✔
720
     * @returns {String} - String with quotes removed.
1✔
721
     */
1✔
722
    static extractStringConstant(value) {
1✔
723
        if (value.startsWith('"') && value.endsWith('"'))
205✔
724
            return value.replace(/"/g, '');
205✔
725

21✔
726
        if (value.startsWith("'") && value.endsWith("'"))
205✔
727
            return value.replace(/'/g, '');
205✔
728

×
729
        return value;
×
730
    }
205✔
731

1✔
732
    /**
1✔
733
     * Convert input into milliseconds.
1✔
734
     * @param {any} value - date as as Date or String.
1✔
735
     * @returns {Number} - date as ms.
1✔
736
     */
1✔
737
    static dateToMs(value) {
1✔
738
        let year = 0;
692✔
739
        let month = 0;
692✔
740
        let dayNum = 0;
692✔
741

692✔
742
        if (value instanceof Date) {
692✔
743
            year = value.getFullYear();
346✔
744
            month = value.getMonth();
346✔
745
            dayNum = value.getDate();
346✔
746
        }
346✔
747
        else if (typeof value === "string") {
346✔
748
            const dateParts = value.split("/");
346✔
749
            if (dateParts.length === 3) {
346✔
750
                year = parseInt(dateParts[2], 10);
346✔
751
                month = parseInt(dateParts[0], 10) - 1;
346✔
752
                dayNum = parseInt(dateParts[1], 10);
346✔
753
            }
346✔
754
        }
346✔
755

692✔
756
        const newDate = new Date(Date.UTC(year, month, dayNum, 12, 0, 0, 0));
692✔
757
        return newDate.getTime();
692✔
758
    }
692✔
759

1✔
760
    /**
1✔
761
     * Compare strings in LIKE condition
1✔
762
     * @param {String} leftValue - string for comparison
1✔
763
     * @param {String} rightValue - string with wildcard
1✔
764
     * @returns {Boolean} - Do strings match?
1✔
765
     */
1✔
766
    static likeCondition(leftValue, rightValue) {
1✔
767
        // @ts-ignore
71✔
768
        const expanded = rightValue.replace(/%/g, ".*").replace(/_/g, ".");
71✔
769

71✔
770
        const result = leftValue.search(expanded);
71✔
771
        return result !== -1;
71✔
772
    }
71✔
773

1✔
774
    /**
1✔
775
     * Check if leftValue is contained in list in rightValue
1✔
776
     * @param {any} leftValue - value to find in right value
1✔
777
     * @param {String} rightValue - list of comma separated values
1✔
778
     * @returns {Boolean} - Is contained IN list.
1✔
779
     */
1✔
780
    static inCondition(leftValue, rightValue) {
1✔
781
        const items = rightValue.split(",");
165✔
782
        for (let i = 0; i < items.length; i++)
165✔
783
            items[i] = items[i].trimStart().trimEnd();
165✔
784

165✔
785
        let index = items.indexOf(leftValue);
165✔
786
        if (index === -1 && typeof leftValue === 'number') {
165!
787
            index = items.indexOf(leftValue.toString());
×
788
        }
×
789

165✔
790
        return index !== -1;
165✔
791
    }
165✔
792

1✔
793
    /**
1✔
794
     * If leftValue is empty (we will consider that as NULL), condition will be true
1✔
795
     * @param {any} leftValue - test this value for NULL
1✔
796
     * @param {any} rightValue - 'NULL' considered as NULL.
1✔
797
     * @returns {Boolean} - Is leftValue NULL (like).
1✔
798
     */
1✔
799
    static isCondition(leftValue, rightValue) {
1✔
800
        return (leftValue === "" && rightValue === "NULL");
10!
801
    }
10✔
802

1✔
803
    /**
1✔
804
     * Test if input is not empty
1✔
805
     * @param {*} rightValue - value to check if empty
1✔
806
     * @returns - true if NOT empty
1✔
807
     */
1✔
808
    static existsCondition(rightValue) {
1✔
809
        return rightValue !== '';
14✔
810
    }
14✔
811

1✔
812
    /**
1✔
813
     * Return a list of column titles for this table.
1✔
814
     * @returns {String[]} - column titles
1✔
815
     */
1✔
816
    getColumnTitles() {
1✔
817
        return this.tableFields.getColumnTitles();
108✔
818
    }
108✔
819
}
1✔
820

1✔
821
/** Evaulate calculated fields in SELECT statement.  This is achieved by converting the request 
1✔
822
 * into javascript and then using 'Function' to evaulate it.  
1✔
823
 */
1✔
824
class CalculatedField {
1✔
825
    /**
1✔
826
     * 
1✔
827
     * @param {Table} masterTable - JOINed table (unless not joined, then primary table)
1✔
828
     * @param {Table} primaryTable - First table in SELECT
1✔
829
     * @param {TableFields} tableFields - All fields from all tables
1✔
830
     */
1✔
831
    constructor(masterTable, primaryTable, tableFields) {
1✔
832
        /** @property {Table} */
425✔
833
        this.masterTable = masterTable;
425✔
834
        /** @property {Table} */
425✔
835
        this.primaryTable = primaryTable;
425✔
836
        /** @property {Map<String,String>} - Map key=calculated field in SELECT, value=javascript equivalent code */ 
425✔
837
        this.sqlServerFunctionCache = new Map();
425✔
838
        /** @property {TableField[]} */
425✔
839
        this.masterFields = tableFields.allFields.filter((vField) => this.masterTable === vField.tableInfo);
425✔
840
    }
425✔
841

1✔
842
    /**
1✔
843
     * Evaluate the calculated field for the current table record and return a value.
1✔
844
     * @param {String} calculatedFormula - calculation from SELECT statement
1✔
845
     * @param {Number} masterRecordID - current record ID.
1✔
846
     * @returns {any} - Evaluated data from calculation.
1✔
847
     */
1✔
848
    evaluateCalculatedField(calculatedFormula, masterRecordID) {
1✔
849
        let result = "";
1,096✔
850

1,096✔
851
        // e.g.  special case.  count(*)
1,096✔
852
        if (calculatedFormula === "*") {
1,096✔
853
            return "*";
45✔
854
        }
45✔
855

1,051✔
856
        const functionString = this.sqlServerCalcFields(calculatedFormula, masterRecordID);
1,051✔
857
        try {
1,051✔
858
            result = new Function(functionString)();
1,051✔
859
        }
1,051✔
860
        catch (ex) {
1,096✔
861
            throw new Error(`Calculated Field Error: ${ex.message}.  ${functionString}`);
12✔
862
        }
12✔
863

1,039✔
864
        return result;
1,039✔
865
    }
1,096✔
866

1✔
867
    /**
1✔
868
     * The program is attempting to build some javascript code which we can then execute to 
1✔
869
     * find the value of the calculated field.  There are two parts.
1✔
870
     * 1)  Build LET statements to assign to all possible field name variants,
1✔
871
     * 2)  Add the 'massaged' calculated field so that it can be run in javascript.
1✔
872
     * @param {String} calculatedFormula - calculation from SELECT statement
1✔
873
     * @param {Number} masterRecordID - current table record ID.
1✔
874
     * @returns {String} - String to be executed.  It is valid javascript lines of code.
1✔
875
     */
1✔
876
    sqlServerCalcFields(calculatedFormula, masterRecordID) {
1✔
877
        //  Working on a calculated field.
1,051✔
878
        const objectsDeclared = new Map();
1,051✔
879
        const variablesDeclared = new Map();
1,051✔
880

1,051✔
881
        let myVars = "";
1,051✔
882
        for (/** @type {TableField} */ const vField of this.masterFields) {
1,051✔
883
            //  Get the DATA from this field.  We then build a series of LET statments
6,896✔
884
            //  and we assign that data to the field name that might be found in a calculated field.
6,896✔
885
            let varData = vField.getData(masterRecordID);
6,896✔
886
            if (typeof varData === "string" || varData instanceof Date) {
6,896✔
887
                varData = `'${varData}'`;
5,122✔
888
            }
5,122✔
889

6,896✔
890
            myVars += this.createAssignmentStatments(vField, objectsDeclared, variablesDeclared, varData);
6,896✔
891
        }
6,896✔
892

1,051✔
893
        const functionString = this.sqlServerFunctions(calculatedFormula);
1,051✔
894

1,051✔
895
        return `${myVars} return ${functionString}`;
1,051✔
896
    }
1,051✔
897

1✔
898
    /**
1✔
899
     * Creates a javascript code block.  For the current field (vField), a variable is assigned the appropriate
1✔
900
     * value from 'varData'.  For example, if the column was 'ID' and the table was 'BOOKS'.
1✔
901
     * ```
1✔
902
     * "let BOOKS = {};BOOKS.ID = '9';"
1✔
903
     * ```
1✔
904
     * If the BOOKS object had already been declared, later variables would just be:
1✔
905
     * ```
1✔
906
     * "BOOKS.NAME = 'To Kill a Blue Jay';"
1✔
907
     * ```
1✔
908
     * @param {TableField} vField - current field that LET statements will be assigning to.
1✔
909
     * @param {Map<String, Boolean>} objectsDeclared - tracks if TABLE name was been encountered yet.
1✔
910
     * @param {Map<String, Boolean>} variablesDeclared - tracks if variables has already been assigned.
1✔
911
     * @param {String} varData - the data from the table that will be assigned to the variable.
1✔
912
     * @returns {String} - the javascript code block.
1✔
913
     */
1✔
914
    createAssignmentStatments(vField, objectsDeclared, variablesDeclared, varData) {
1✔
915
        let myVars = "";
6,896✔
916

6,896✔
917
        for (const aliasName of vField.aliasNames) {
6,896✔
918
            if ((this.primaryTable.tableName !== vField.tableInfo.tableName && aliasName.indexOf(".") === -1))
13,792✔
919
                continue;
13,792✔
920

12,928✔
921
            if (aliasName.indexOf(".") === -1) {
13,792✔
922
                if (!variablesDeclared.has(aliasName)) {
6,032✔
923
                    myVars += `let ${aliasName} = ${varData};`;
5,952✔
924
                    variablesDeclared.set(aliasName, true);
5,952✔
925
                }
5,952✔
926
            }
6,032✔
927
            else {
6,896✔
928
                const parts = aliasName.split(".");
6,896✔
929
                if (!objectsDeclared.has(parts[0])) {
6,896✔
930
                    myVars += `let ${parts[0]} = {};`;
1,192✔
931
                    objectsDeclared.set(parts[0], true);
1,192✔
932
                }
1,192✔
933
                myVars += `${aliasName} = ${varData};`;
6,896✔
934
            }
6,896✔
935
        }
13,792✔
936

6,896✔
937
        return myVars;
6,896✔
938
    }
6,896✔
939

1✔
940
    /**
1✔
941
     * 
1✔
942
     * @param {String} calculatedFormula 
1✔
943
     * @returns {String}
1✔
944
     */
1✔
945
    sqlServerFunctions(calculatedFormula) {
1✔
946
        //  If this calculated field formula has already been put into the required format,
1,051✔
947
        //  pull this out of our cache rather than redo.
1,051✔
948
        if (this.sqlServerFunctionCache.has(calculatedFormula))
1,051✔
949
            return this.sqlServerFunctionCache.get(calculatedFormula);
1,051✔
950

124✔
951
        const func = new SqlServerFunctions();
124✔
952
        const functionString = func.convertToJs(calculatedFormula, this.masterFields);
124✔
953

124✔
954
        //  No need to recalculate for each row.
124✔
955
        this.sqlServerFunctionCache.set(calculatedFormula, functionString);
124✔
956

124✔
957
        return functionString;
124✔
958
    }
1,051✔
959
}
1✔
960

1✔
961
/** Correlated Sub-Query requires special lookups for every record in the primary table. */
1✔
962
class CorrelatedSubQuery {
1✔
963
    /**
1✔
964
     * 
1✔
965
     * @param {Map<String, Table>} tableInfo - Map of table info.
1✔
966
     * @param {TableFields} tableFields - Fields from all tables.
1✔
967
     * @param {Object} defaultSubQuery - Select AST
1✔
968
     */
1✔
969
    constructor(tableInfo, tableFields, defaultSubQuery = null) {
1✔
970
        /** @property {Map<String, Table>} - Map of table info. */
196✔
971
        this.tableInfo = tableInfo;
196✔
972
        /** @property {TableFields} - Fields from all tables.*/
196✔
973
        this.tableFields = tableFields;
196✔
974
        /** @property {Object} - AST can be set here and skipped in select() statement. */
196✔
975
        this.defaultSubQuery = defaultSubQuery;
196✔
976
    }
196✔
977

1✔
978
    /**
1✔
979
     * Perform SELECT on sub-query using data from current record in outer table.
1✔
980
     * @param {Number} masterRecordID - Current record number in outer table.
1✔
981
     * @param {CalculatedField} calcSqlField - Calculated field object.
1✔
982
     * @param {Object} ast - Sub-query AST.
1✔
983
     * @returns {any[][]} - double array of selected table data.
1✔
984
     */
1✔
985
    select(masterRecordID, calcSqlField, ast = this.defaultSubQuery) {
1✔
986
        const inSQL = new Sql().setTables(this.tableInfo);
34✔
987

34✔
988
        const innerTableInfo = this.tableInfo.get(ast.FROM[0].table.toUpperCase());
34✔
989
        if (typeof innerTableInfo === 'undefined')
34✔
990
            throw new Error(`No table data found: ${ast.FROM[0].table}`);
34!
991

34✔
992
        //  Add BIND variable for all matching fields in WHERE.
34✔
993
        const tempAst = JSON.parse(JSON.stringify(ast));
34✔
994

34✔
995
        const bindVariables = this.replaceOuterFieldValueInCorrelatedWhere(calcSqlField.masterFields, masterRecordID, tempAst);
34✔
996

34✔
997
        inSQL.setBindValues(bindVariables);
34✔
998
        const inData = inSQL.select(tempAst);
34✔
999

34✔
1000
        return inData;
34✔
1001
    }
34✔
1002

1✔
1003
    /**
1✔
1004
     * If we find the field name in the AST, just replace with '?' and add to bind data variable list.
1✔
1005
     * @param {TableField[]} fieldNames - List of fields in outer query.  If any are found in subquery, the value of that field for the current record is inserted into subquery before it is executed.
1✔
1006
     * @param {Number} masterRecordID - current record number in outer query.
1✔
1007
     * @param {Object} tempAst - AST for subquery.  Any field names found from outer query will be replaced with bind place holder '?'.
1✔
1008
     * @returns {any[]} - Data from outer query to be used as bind variable data.
1✔
1009
     */
1✔
1010
    replaceOuterFieldValueInCorrelatedWhere(fieldNames, masterRecordID, tempAst) {
1✔
1011
        const where = tempAst.WHERE;
34✔
1012

34✔
1013
        if (typeof where === 'undefined')
34✔
1014
            return [];
34!
1015

34✔
1016
        let bindData = [];
34✔
1017
        if (typeof where.logic === 'undefined')
34✔
1018
            bindData = this.traverseWhere(fieldNames, [where]);
34✔
1019
        else
×
1020
            bindData = this.traverseWhere(fieldNames, where.terms);
×
1021

34✔
1022
        for (let i = 0; i < bindData.length; i++) {
34✔
1023
            const fldName = bindData[i];
34✔
1024
            for (const vField of fieldNames) {
34✔
1025
                if (fldName === vField.fieldName) {
34✔
1026
                    bindData[i] = vField.getData(masterRecordID);
34✔
1027
                    break;
34✔
1028
                }
34✔
1029
            }
34✔
1030
        }
34✔
1031

34✔
1032
        return bindData;
34✔
1033
    }
34✔
1034

1✔
1035
    /**
1✔
1036
     * Search the WHERE portion of the subquery to find all references to the table in the outer query.
1✔
1037
     * @param {TableField[]} fieldNames - List of fields in outer query.
1✔
1038
     * @param {Object} terms - terms of WHERE.  It is modified with bind variable placeholders when outer table fields are located.
1✔
1039
     * @returns {String[]} - List of field names found from outer table in subquery.
1✔
1040
     */
1✔
1041
    traverseWhere(fieldNames, terms) {
1✔
1042
        const bindFields = [];
34✔
1043

34✔
1044
        for (const cond of terms) {
34✔
1045
            if (typeof cond.logic === 'undefined') {
34✔
1046
                let result = fieldNames.find(item => item.fieldName === cond.left.toUpperCase());
34✔
1047
                if (typeof result !== 'undefined') {
34✔
1048
                    bindFields.push(cond.left.toUpperCase());
20✔
1049
                    cond.left = '?';
20✔
1050
                }
20✔
1051
                result = fieldNames.find(item => item.fieldName === cond.right.toUpperCase());
34✔
1052
                if (typeof result !== 'undefined') {
34✔
1053
                    bindFields.push(cond.right.toUpperCase());
14✔
1054
                    cond.right = '?';
14✔
1055
                }
14✔
1056
            }
34✔
1057
            else {
×
1058
                bindFields.push(fieldNames, this.traverseWhere([cond.terms]));
×
1059
            }
×
1060
        }
34✔
1061

34✔
1062
        return bindFields;
34✔
1063
    }
34✔
1064
}
1✔
1065

1✔
1066
/** Tracks all fields in a table (including derived tables when there is a JOIN). */
1✔
1067
class VirtualFields {
1✔
1068
    constructor() {
1✔
1069
        /** @property {Map<String, VirtualField>} - Map to field for fast access. Field name is key. */
716✔
1070
        this.virtualFieldMap = new Map();
716✔
1071
        /** @property {VirtualField[]} - List of all fields for table. */
716✔
1072
        this.virtualFieldList = [];
716✔
1073
    }
716✔
1074

1✔
1075
    /**
1✔
1076
     * Adds info for one field into master list of fields for table.
1✔
1077
     * @param {VirtualField} field - Information for one field in the table.
1✔
1078
     */
1✔
1079
    add(field, checkForDuplicates = false) {
1✔
1080
        if (checkForDuplicates && this.virtualFieldMap.has(field.fieldName)) {
2,744✔
1081
            throw new Error(`Duplicate field name: ${field.fieldName}`);
1✔
1082
        }
1✔
1083
        this.virtualFieldMap.set(field.fieldName, field);
2,743✔
1084
        this.virtualFieldList.push(field);
2,743✔
1085
    }
2,744✔
1086

1✔
1087
    /**
1✔
1088
     * Returns a list of all fields in table.
1✔
1089
     * @returns {VirtualField[]}
1✔
1090
     */
1✔
1091
    getAllVirtualFields() {
1✔
1092
        return this.virtualFieldList;
41✔
1093
    }
41✔
1094

1✔
1095
    /**
1✔
1096
     * When the wildcard '*' is found in the SELECT, it will add all fields in table to the AST used in the SELECT.
1✔
1097
     * @param {Table} masterTableInfo - The wildcard '*' (if found) will add fields from THIS table to the AST.
1✔
1098
     * @param {any[]} astFields - existing SELECT fields list.
1✔
1099
     * @returns {any[]} - original AST field list PLUS expanded list of fields if '*' was encountered.
1✔
1100
     */
1✔
1101
    static expandWildcardFields(masterTableInfo, astFields) {
1✔
1102
        for (let i = 0; i < astFields.length; i++) {
205✔
1103
            if (astFields[i].name === "*") {
528✔
1104
                //  Replace wildcard will actual field names from master table.
53✔
1105
                const masterTableFields = [];
53✔
1106
                const allExpandedFields = masterTableInfo.getAllExtendedNotationFieldNames();
53✔
1107

53✔
1108
                for (const virtualField of allExpandedFields) {
53✔
1109
                    const selField = { name: virtualField };
265✔
1110
                    masterTableFields.push(selField);
265✔
1111
                }
265✔
1112

53✔
1113
                astFields.splice(i, 1, ...masterTableFields);
53✔
1114
                break;
53✔
1115
            }
53✔
1116
        }
528✔
1117

205✔
1118
        return astFields;
205✔
1119
    }
205✔
1120
}
1✔
1121

1✔
1122
/**  Defines all possible table fields including '*' and long/short form (i.e. table.column). */
1✔
1123
class VirtualField {                        //  skipcq: JS-0128
1✔
1124
    /**
1✔
1125
     * 
1✔
1126
     * @param {String} fieldName - field name
1✔
1127
     * @param {Table} tableInfo - table this field belongs to.
1✔
1128
     * @param {Number} tableColumn - column number of this field.
1✔
1129
     */
1✔
1130
    constructor(fieldName, tableInfo, tableColumn) {
1✔
1131
        /** @property {String} - field name */
2,744✔
1132
        this.fieldName = fieldName;
2,744✔
1133
        /** @property {Table} - table this field belongs to. */
2,744✔
1134
        this.tableInfo = tableInfo;
2,744✔
1135
        /** @property {Number} - column number of this field. */
2,744✔
1136
        this.tableColumn = tableColumn;
2,744✔
1137
    }
2,744✔
1138
}
1✔
1139

1✔
1140
/** Handle the various JOIN table types. */
1✔
1141
class JoinTables {
1✔
1142
    /**
1✔
1143
     * Join the tables and create a derived table with the combined data from all.
1✔
1144
     * @param {any[]} astJoin - AST list of tables to join.
1✔
1145
     * @param {TableFields} tableFields
1✔
1146
     */
1✔
1147
    constructor(astJoin, tableFields = null) {
1✔
1148
        /** @property {DerivedTable} - result table after tables are joined */
233✔
1149
        this.derivedTable = new DerivedTable();
233✔
1150

233✔
1151
        for (const joinTable of astJoin) {
233✔
1152
            /** @type {TableField} */
44✔
1153
            let leftFieldInfo = null;
44✔
1154
            /** @type {TableField} */
44✔
1155
            let rightFieldInfo = null;
44✔
1156
            if (tableFields !== null) {
44✔
1157
                if (typeof joinTable.cond.left === 'undefined' || typeof joinTable.cond.right === 'undefined') {
44✔
1158
                    throw new Error("Invalid JOIN TABLE ON syntax");
1✔
1159
                }
1✔
1160
                leftFieldInfo = tableFields.getFieldInfo(joinTable.cond.left);
43✔
1161
                rightFieldInfo = tableFields.getFieldInfo(joinTable.cond.right);
43✔
1162
            }
43✔
1163

43✔
1164
            this.derivedTable = JoinTables.joinTables(leftFieldInfo, rightFieldInfo, joinTable);
43✔
1165

43✔
1166
            //  Field locations have changed to the derived table, so update our
43✔
1167
            //  virtual field list with proper settings.
43✔
1168
            tableFields.updateDerivedTableVirtualFields(this.derivedTable);
43✔
1169
        }
43✔
1170
    }
233✔
1171

1✔
1172
    /**
1✔
1173
     * Does this object contain a derived (joined) table.
1✔
1174
     * @returns {Boolean}
1✔
1175
     */
1✔
1176
    isDerivedTable() {
1✔
1177
        return this.derivedTable.isDerivedTable();
232✔
1178
    }
232✔
1179

1✔
1180
    /**
1✔
1181
     * Get derived table after tables are joined.
1✔
1182
     * @returns {Table}
1✔
1183
     */
1✔
1184
    getJoinedTableInfo() {
1✔
1185
        return this.derivedTable.getTableData();
25✔
1186
    }
25✔
1187

1✔
1188
    /**
1✔
1189
    * Join two tables and create a derived table that contains all data from both tables.
1✔
1190
    * @param {TableField} leftFieldInfo - left table field of join
1✔
1191
    * @param {TableField} rightFieldInfo - right table field of join
1✔
1192
    * @param {Object} joinTable - AST that contains join type.
1✔
1193
    * @returns {DerivedTable} - new derived table after join of left and right tables.
1✔
1194
    */
1✔
1195
    static joinTables(leftFieldInfo, rightFieldInfo, joinTable) {
1✔
1196
        let matchedRecordIDs = [];
43✔
1197
        let leftJoinRecordIDs = [];
43✔
1198
        let rightJoinRecordIDs = [];
43✔
1199
        let derivedTable = null;
43✔
1200
        let rightDerivedTable = null;
43✔
1201

43✔
1202
        switch (joinTable.type) {
43✔
1203
            case "left":
43✔
1204
                matchedRecordIDs = JoinTables.leftRightJoin(leftFieldInfo, rightFieldInfo, joinTable.type);
24✔
1205
                derivedTable = new DerivedTable()
24✔
1206
                    .setLeftField(leftFieldInfo)
24✔
1207
                    .setRightField(rightFieldInfo)
24✔
1208
                    .setLeftRecords(matchedRecordIDs)
24✔
1209
                    .setIsOuterJoin(true)
24✔
1210
                    .createTable();
24✔
1211
                break;
24✔
1212

43✔
1213
            case "inner":
43✔
1214
                matchedRecordIDs = JoinTables.leftRightJoin(leftFieldInfo, rightFieldInfo, joinTable.type);
11✔
1215
                derivedTable = new DerivedTable()
11✔
1216
                    .setLeftField(leftFieldInfo)
11✔
1217
                    .setRightField(rightFieldInfo)
11✔
1218
                    .setLeftRecords(matchedRecordIDs)
11✔
1219
                    .setIsOuterJoin(false)
11✔
1220
                    .createTable();
11✔
1221
                break;
11✔
1222

43✔
1223
            case "right":
43✔
1224
                matchedRecordIDs = JoinTables.leftRightJoin(rightFieldInfo, leftFieldInfo, joinTable.type);
3✔
1225
                derivedTable = new DerivedTable()
3✔
1226
                    .setLeftField(rightFieldInfo)
3✔
1227
                    .setRightField(leftFieldInfo)
3✔
1228
                    .setLeftRecords(matchedRecordIDs)
3✔
1229
                    .setIsOuterJoin(true)
3✔
1230
                    .createTable();
3✔
1231

3✔
1232
                break;
3✔
1233

43✔
1234
            case "full":
43✔
1235
                leftJoinRecordIDs = JoinTables.leftRightJoin(leftFieldInfo, rightFieldInfo, joinTable.type);
5✔
1236
                derivedTable = new DerivedTable()
5✔
1237
                    .setLeftField(leftFieldInfo)
5✔
1238
                    .setRightField(rightFieldInfo)
5✔
1239
                    .setLeftRecords(leftJoinRecordIDs)
5✔
1240
                    .setIsOuterJoin(true)
5✔
1241
                    .createTable();
5✔
1242

5✔
1243
                rightJoinRecordIDs = JoinTables.leftRightJoin(rightFieldInfo, leftFieldInfo, "outer");
5✔
1244
                rightDerivedTable = new DerivedTable()
5✔
1245
                    .setLeftField(rightFieldInfo)
5✔
1246
                    .setRightField(leftFieldInfo)
5✔
1247
                    .setLeftRecords(rightJoinRecordIDs)
5✔
1248
                    .setIsOuterJoin(true)
5✔
1249
                    .createTable();
5✔
1250

5✔
1251
                derivedTable.tableInfo.concat(rightDerivedTable.tableInfo);
5✔
1252

5✔
1253
                break;
5✔
1254

43✔
1255
            default:
43!
1256
                throw new Error(`Internal error.  No support for join type: ${joinTable.type}`);
×
1257
        }
43✔
1258
        return derivedTable;
41✔
1259
    }
43✔
1260

1✔
1261
    /**
1✔
1262
     * Returns array of each matching record ID from right table for every record in left table.
1✔
1263
     * If the right table entry could NOT be found, -1 is set for that record index.
1✔
1264
     * @param {TableField} leftField - left table field
1✔
1265
     * @param {TableField} rightField - right table field
1✔
1266
     * @param {String} type - either 'inner' or 'outer'.
1✔
1267
     * @returns {Number[][]} - first index is record ID of left table, second index is a list of the matching record ID's in right table.
1✔
1268
     */
1✔
1269
    static leftRightJoin(leftField, rightField, type) {
1✔
1270
        const leftRecordsIDs = [];
48✔
1271

48✔
1272
        //  First record is the column title.
48✔
1273
        leftRecordsIDs.push([0]);
48✔
1274

48✔
1275
        /** @type {any[][]} */
48✔
1276
        const leftTableData = leftField.tableInfo.tableData;
48✔
1277
        const leftTableCol = leftField.tableColumn;
48✔
1278

48✔
1279
        rightField.tableInfo.addIndex(rightField.fieldName);
48✔
1280

48✔
1281
        for (let leftTableRecordNum = 1; leftTableRecordNum < leftTableData.length; leftTableRecordNum++) {
48✔
1282
            const keyMasterJoinField = leftTableData[leftTableRecordNum][leftTableCol];
426✔
1283
            const joinRows = rightField.tableInfo.search(rightField.fieldName, keyMasterJoinField);
426✔
1284

426✔
1285
            //  For the current LEFT TABLE record, record the linking RIGHT TABLE records.
426✔
1286
            if (joinRows.length === 0) {
426✔
1287
                if (type === "inner")
97✔
1288
                    continue;
97✔
1289

79✔
1290
                leftRecordsIDs[leftTableRecordNum] = [-1];
79✔
1291
            }
79✔
1292
            else {
329✔
1293
                //  Excludes all match recordgs (is outer the right word for this?)
329✔
1294
                if (type === "outer")
329✔
1295
                    continue;
329✔
1296

312✔
1297
                leftRecordsIDs[leftTableRecordNum] = joinRows;
312✔
1298
            }
312✔
1299
        }
426✔
1300

46✔
1301
        return leftRecordsIDs;
46✔
1302
    }
48✔
1303
}
1✔
1304

1✔
1305
/**  The JOIN creates a new logical table. */
1✔
1306
class DerivedTable {
1✔
1307
    constructor() {
1✔
1308
        /** @property {Table} */
279✔
1309
        this.tableInfo = null;
279✔
1310
        /** @property  {TableField} */
279✔
1311
        this.leftField = null;
279✔
1312
        /** @property  {TableField} */
279✔
1313
        this.rightField = null;
279✔
1314
        /** @property  {Number[][]} */
279✔
1315
        this.leftRecords = null;
279✔
1316
        /** @property  {Boolean} */
279✔
1317
        this.isOuterJoin = null;
279✔
1318
    }
279✔
1319

1✔
1320
    /**
1✔
1321
     * Left side of join condition.
1✔
1322
     * @param {TableField} leftField 
1✔
1323
     * @returns {DerivedTable}
1✔
1324
     */
1✔
1325
    setLeftField(leftField) {
1✔
1326
        this.leftField = leftField;
46✔
1327
        return this;
46✔
1328
    }
46✔
1329

1✔
1330
    /**
1✔
1331
     * Right side of join condition
1✔
1332
     * @param {TableField} rightField 
1✔
1333
     * @returns {DerivedTable}
1✔
1334
     */
1✔
1335
    setRightField(rightField) {
1✔
1336
        this.rightField = rightField;
46✔
1337
        return this;
46✔
1338
    }
46✔
1339

1✔
1340
    /**
1✔
1341
     * 
1✔
1342
     * @param {Number[][]} leftRecords - first index is record ID of left table, second index is a list of the matching record ID's in right table.
1✔
1343
     * @returns {DerivedTable} 
1✔
1344
     */
1✔
1345
    setLeftRecords(leftRecords) {
1✔
1346
        this.leftRecords = leftRecords;
46✔
1347
        return this;
46✔
1348
    }
46✔
1349

1✔
1350
    /**
1✔
1351
     * Indicate if outer or inner join.
1✔
1352
     * @param {Boolean} isOuterJoin - true for outer, false for inner
1✔
1353
     * @returns {DerivedTable}
1✔
1354
     */
1✔
1355
    setIsOuterJoin(isOuterJoin) {
1✔
1356
        this.isOuterJoin = isOuterJoin;
46✔
1357
        return this;
46✔
1358
    }
46✔
1359

1✔
1360
    /**
1✔
1361
     * Create derived table from the two tables that are joined.
1✔
1362
     * @returns {DerivedTable}
1✔
1363
     */
1✔
1364
    createTable() {
1✔
1365
        const columnCount = this.rightField.tableInfo.getColumnCount();
46✔
1366
        const emptyRightRow = Array(columnCount).fill("");
46✔
1367

46✔
1368
        const joinedData = [DerivedTable.getCombinedColumnTitles(this.leftField, this.rightField)];
46✔
1369

46✔
1370
        for (let i = 1; i < this.leftField.tableInfo.tableData.length; i++) {
46✔
1371
            if (typeof this.leftRecords[i] !== "undefined") {
426✔
1372
                if (typeof this.rightField.tableInfo.tableData[this.leftRecords[i][0]] === "undefined")
391✔
1373
                    joinedData.push(this.leftField.tableInfo.tableData[i].concat(emptyRightRow));
391✔
1374
                else {
312✔
1375
                    const maxJoin = this.isOuterJoin ? this.leftRecords[i].length : 1;
312✔
1376
                    for (let j = 0; j < maxJoin; j++) {
312✔
1377
                        joinedData.push(this.leftField.tableInfo.tableData[i].concat(this.rightField.tableInfo.tableData[this.leftRecords[i][j]]));
319✔
1378
                    }
319✔
1379
                }
312✔
1380
            }
391✔
1381
        }
426✔
1382
        /** @type {Table} */
46✔
1383
        this.tableInfo = new Table(DERIVEDTABLE).loadArrayData(joinedData);
46✔
1384

46✔
1385
        return this;
46✔
1386
    }
46✔
1387

1✔
1388
    /**
1✔
1389
    * Is this a derived table - one that has been joined.
1✔
1390
    * @returns {Boolean}
1✔
1391
    */
1✔
1392
    isDerivedTable() {
1✔
1393
        return this.tableInfo !== null;
232✔
1394
    }
232✔
1395

1✔
1396
    /**
1✔
1397
     * Get derived table info.
1✔
1398
     * @returns {Table}
1✔
1399
     */
1✔
1400
    getTableData() {
1✔
1401
        return this.tableInfo;
25✔
1402
    }
25✔
1403

1✔
1404
    /**
1✔
1405
     * Create title row from LEFT and RIGHT table.
1✔
1406
     * @param {TableField} leftField 
1✔
1407
     * @param {TableField} rightField 
1✔
1408
     * @returns {String[]}
1✔
1409
     */
1✔
1410
    static getCombinedColumnTitles(leftField, rightField) {
1✔
1411
        const titleRow = leftField.tableInfo.getAllExtendedNotationFieldNames();
46✔
1412
        const rightFieldNames = rightField.tableInfo.getAllExtendedNotationFieldNames();
46✔
1413
        return titleRow.concat(rightFieldNames);
46✔
1414
    }
46✔
1415
}
1✔
1416

1✔
1417
/** Convert SQL CALCULATED fields into javascript code that can be evaulated and converted to data. */
1✔
1418
class SqlServerFunctions {
1✔
1419
    /**
1✔
1420
     * Convert SQL formula to javascript code.
1✔
1421
     * @param {String} calculatedFormula - contains SQL formula and parameter(s)
1✔
1422
     * @param {TableField[]} masterFields - table fields
1✔
1423
     * @returns {String} - javascript code
1✔
1424
     */
1✔
1425
    convertToJs(calculatedFormula, masterFields) {
1✔
1426
        const sqlFunctions = ["ABS", "CASE", "CEILING", "CHARINDEX", "COALESCE", "CONCAT_WS", "DAY", "FLOOR", "IF", "LEFT", "LEN", "LENGTH", "LOG", "LOG10", "LOWER",
124✔
1427
            "LTRIM", "MONTH", "NOW", "POWER", "RAND", "REPLICATE", "REVERSE", "RIGHT", "ROUND", "RTRIM",
124✔
1428
            "SPACE", "STUFF", "SUBSTRING", "SQRT", "TRIM", "UPPER", "YEAR"];
124✔
1429
        /** @property {String} - regex to find components of CASE statement. */
124✔
1430
        this.matchCaseWhenThenStr = /WHEN(.*?)THEN(.*?)(?=WHEN|ELSE|$)|ELSE(.*?)(?=$)/;
124✔
1431
        /** @property {String} - Original CASE statement. */
124✔
1432
        this.originalCaseStatement = "";
124✔
1433
        /** @property {String} - Existing state of function string when CASE encountered. */
124✔
1434
        this.originalFunctionString = "";
124✔
1435
        /** @property {Boolean} - when working on each WHEN/THEN in CASE, is this the first one encountered. */
124✔
1436
        this.firstCase = true;
124✔
1437

124✔
1438
        let functionString = SelectTables.toUpperCaseExceptQuoted(calculatedFormula);
124✔
1439

124✔
1440
        for (const func of sqlFunctions) {
124✔
1441
            let args = SelectTables.parseForFunctions(functionString, func);
3,968✔
1442

3,968✔
1443
            [args, functionString] = this.caseStart(func, args, functionString);
3,968✔
1444

3,968✔
1445
            while (args !== null && args.length > 0) {
3,968✔
1446
                // Split on COMMA, except within brackets.
148✔
1447
                const parms = typeof args[1] === 'undefined' ? [] : SelectTables.parseForParams(args[1]);
148✔
1448

148✔
1449
                let replacement = "";
148✔
1450
                switch (func) {
148✔
1451
                    case "ABS":
148✔
1452
                        replacement = `Math.abs(${parms[0]})`;
1✔
1453
                        break;
1✔
1454
                    case "CASE":
148✔
1455
                        replacement = this.caseWhen(args);
90✔
1456
                        break;
90✔
1457
                    case "CEILING":
148✔
1458
                        replacement = `Math.ceil(${parms[0]})`;
1✔
1459
                        break;
1✔
1460
                    case "CHARINDEX":
148✔
1461
                        replacement = SqlServerFunctions.charIndex(parms);
6✔
1462
                        break;
6✔
1463
                    case "COALESCE":
148✔
1464
                        replacement = SqlServerFunctions.coalesce(parms);
1✔
1465
                        break;
1✔
1466
                    case "CONCAT_WS":
148✔
1467
                        replacement = SqlServerFunctions.concat_ws(parms, masterFields);
4✔
1468
                        break;
4✔
1469
                    case "DAY":
148✔
1470
                        replacement = `new Date(${parms[0]}).getDate()`;
1✔
1471
                        break;
1✔
1472
                    case "FLOOR":
148✔
1473
                        replacement = `Math.floor(${parms[0]})`;
1✔
1474
                        break;
1✔
1475
                    case "IF":
148✔
1476
                        {
8✔
1477
                            const ifCond = SqlParse.sqlCondition2JsCondition(parms[0]);
8✔
1478
                            replacement = `${ifCond} ? ${parms[1]} : ${parms[2]};`;
8✔
1479
                            break;
8✔
1480
                        }
8✔
1481
                    case "LEFT":
148✔
1482
                        replacement = `${parms[0]}.substring(0,${parms[1]})`;
1✔
1483
                        break;
1✔
1484
                    case "LEN":
148✔
1485
                    case "LENGTH":
148✔
1486
                        replacement = `${parms[0]}.length`;
2✔
1487
                        break;
2✔
1488
                    case "LOG":
148✔
1489
                        replacement = `Math.log2(${parms[0]})`;
1✔
1490
                        break;
1✔
1491
                    case "LOG10":
148✔
1492
                        replacement = `Math.log10(${parms[0]})`;
1✔
1493
                        break;
1✔
1494
                    case "LOWER":
148✔
1495
                        replacement = `${parms[0]}.toLowerCase()`;
3✔
1496
                        break;
3✔
1497
                    case "LTRIM":
148✔
1498
                        replacement = `${parms[0]}.trimStart()`;
1✔
1499
                        break;
1✔
1500
                    case "MONTH":
148✔
1501
                        replacement = `new Date(${parms[0]}).getMonth() + 1`;
1✔
1502
                        break;
1✔
1503
                    case "NOW":
148✔
1504
                        replacement = "new Date().toLocaleString()";
1✔
1505
                        break;
1✔
1506
                    case "POWER":
148✔
1507
                        replacement = `Math.pow(${parms[0]},${parms[1]})`;
1✔
1508
                        break;
1✔
1509
                    case "RAND":
148!
1510
                        replacement = "Math.random()";
×
1511
                        break;
×
1512
                    case "REPLICATE":
148✔
1513
                        replacement = `${parms[0]}.repeat(${parms[1]})`;
1✔
1514
                        break;
1✔
1515
                    case "REVERSE":
148✔
1516
                        replacement = `${parms[0]}.split("").reverse().join("")`;
1✔
1517
                        break;
1✔
1518
                    case "RIGHT":
148✔
1519
                        replacement = `${parms[0]}.slice(${parms[0]}.length - ${parms[1]})`;
1✔
1520
                        break;
1✔
1521
                    case "ROUND":
148✔
1522
                        replacement = `Math.round(${parms[0]})`;
2✔
1523
                        break;
2✔
1524
                    case "RTRIM":
148✔
1525
                        replacement = `${parms[0]}.trimEnd()`;
1✔
1526
                        break;
1✔
1527
                    case "SPACE":
148✔
1528
                        replacement = `' '.repeat(${parms[0]})`;
1✔
1529
                        break;
1✔
1530
                    case "STUFF":
148✔
1531
                        replacement = `${parms[0]}.substring(0,${parms[1]}-1) + ${parms[3]} + ${parms[0]}.substring(${parms[1]} + ${parms[2]} - 1)`;
2✔
1532
                        break;
2✔
1533
                    case "SUBSTRING":
148✔
1534
                        replacement = `${parms[0]}.substring(${parms[1]} - 1, ${parms[1]} + ${parms[2]} - 1)`;
5✔
1535
                        break;
5✔
1536
                    case "SQRT":
148✔
1537
                        replacement = `Math.sqrt(${parms[0]})`;
1✔
1538
                        break;
1✔
1539
                    case "TRIM":
148✔
1540
                        replacement = `${parms[0]}.trim()`;
3✔
1541
                        break;
3✔
1542
                    case "UPPER":
148✔
1543
                        replacement = `${parms[0]}.toUpperCase()`;
4✔
1544
                        break;
4✔
1545
                    case "YEAR":
148✔
1546
                        replacement = `new Date(${parms[0]}).getFullYear()`;
1✔
1547
                        break;
1✔
1548
                    default:
148!
1549
                        throw new Error(`Internal Error. Function is missing. ${func}`);
×
1550
                }
148✔
1551

148✔
1552
                functionString = functionString.replace(args[0], replacement);
148✔
1553

148✔
1554
                args = this.parseFunctionArgs(func, functionString);
148✔
1555
            }
148✔
1556

3,968✔
1557
            functionString = this.caseEnd(func, functionString);
3,968✔
1558
        }
3,968✔
1559

124✔
1560
        return functionString;
124✔
1561
    }
124✔
1562

1✔
1563
    /**
1✔
1564
     * Search for SELECT function arguments for specified 'func' only.  Special case for 'CASE'.  It breaks down one WHEN condition at a time.
1✔
1565
     * @param {String} func - an SQL function name.
1✔
1566
     * @param {String} functionString - SELECT SQL string to search
1✔
1567
     * @returns {String[]}
1✔
1568
     */
1✔
1569
    parseFunctionArgs(func, functionString) {
1✔
1570
        let args = [];
148✔
1571

148✔
1572
        if (func === "CASE")
148✔
1573
            args = functionString.match(this.matchCaseWhenThenStr);
148✔
1574
        else
58✔
1575
            args = SelectTables.parseForFunctions(functionString, func);
58✔
1576

148✔
1577
        return args;
148✔
1578
    }
148✔
1579

1✔
1580
    /**
1✔
1581
     * Find the position of a substring within a field - in javascript code.
1✔
1582
     * @param {any[]} parms - 
1✔
1583
     * * parms[0] - string to search for
1✔
1584
     * * parms[1] - field name
1✔
1585
     * * parms[2] - start to search from this position (starts at 1)
1✔
1586
     * @returns {String} - javascript code to find substring position.
1✔
1587
     */
1✔
1588
    static charIndex(parms) {
1✔
1589
        let replacement = "";
6✔
1590

6✔
1591
        if (typeof parms[2] === 'undefined')
6✔
1592
            replacement = `${parms[1]}.indexOf(${parms[0]}) + 1`;
6✔
1593
        else
3✔
1594
            replacement = `${parms[1]}.indexOf(${parms[0]},${parms[2]} -1) + 1`;
3✔
1595

6✔
1596
        return replacement;
6✔
1597
    }
6✔
1598

1✔
1599
    /**
1✔
1600
     * Returns first non-empty value in a list, in javascript code.
1✔
1601
     * @param {any[]} parms - coalesce parameters - no set limit for number of inputs.
1✔
1602
     * @returns {String} - javascript to solve
1✔
1603
     */
1✔
1604
    static coalesce(parms) {
1✔
1605
        let replacement = "";
1✔
1606
        for (const parm of parms) {
1✔
1607
            replacement += `${parm} !== '' ? ${parm} : `;
12✔
1608
        }
12✔
1609

1✔
1610
        replacement += `''`;
1✔
1611

1✔
1612
        return replacement;
1✔
1613
    }
1✔
1614

1✔
1615
    /**
1✔
1616
     * Concatenate all data and use separator between concatenated fields.
1✔
1617
     * @param {any[]} parms - 
1✔
1618
     * * parm[0] - separator string
1✔
1619
     * * parms... - data to concatenate.
1✔
1620
     * @param {TableField[]} masterFields - fields in table.
1✔
1621
     * @returns {String} - javascript to concatenate all data.
1✔
1622
     */
1✔
1623
    static concat_ws(parms, masterFields) {
1✔
1624
        if (parms.length === 0) {
4!
1625
            return "";
×
1626
        }
×
1627

4✔
1628
        let replacement = "";
4✔
1629
        const separator = parms[0];
4✔
1630
        let concatFields = [];
4✔
1631

4✔
1632
        for (let i = 1; i < parms.length; i++) {
4✔
1633
            if (parms[i].trim() === "*") {
4✔
1634
                const allTableFields = TableField.getAllExtendedAliasNames(masterFields);
4✔
1635
                concatFields = concatFields.concat(allTableFields);
4✔
1636
            }
4✔
1637
            else {
×
1638
                concatFields.push(parms[i]);
×
1639
            }
×
1640
        }
4✔
1641

4✔
1642
        for (const field of concatFields) {
4✔
1643
            if (replacement !== "") {
36✔
1644
                replacement += ` + ${separator} + `;
32✔
1645
            }
32✔
1646

36✔
1647
            replacement += `${field}`;
36✔
1648
        }
36✔
1649

4✔
1650
        return replacement;
4✔
1651
    }
4✔
1652

1✔
1653
    /**
1✔
1654
     * When examining the SQL Select CASE, parse for next WHEN,END condition.
1✔
1655
     * @param {String} func - current function worked on.  If <> 'CASE', ignore.
1✔
1656
     * @param {any[]} args - default return value. 
1✔
1657
     * @param {String} functionString 
1✔
1658
     * @returns {any[]}
1✔
1659
     */
1✔
1660
    caseStart(func, args, functionString) {
1✔
1661
        let caseArguments = args;
3,968✔
1662
        let caseString = functionString;
3,968✔
1663

3,968✔
1664
        if (func === "CASE") {
3,968✔
1665
            caseArguments = functionString.match(/CASE(.*?)END/i);
124✔
1666

124✔
1667
            if (caseArguments !== null && caseArguments.length > 1) {
124✔
1668
                this.firstCase = true;
42✔
1669
                this.originalFunctionString = functionString;
42✔
1670
                this.originalCaseStatement = caseArguments[0];
42✔
1671
                caseString = caseArguments[1];
42✔
1672

42✔
1673
                caseArguments = caseArguments[1].match(this.matchCaseWhenThenStr);
42✔
1674
            }
42✔
1675
        }
124✔
1676

3,968✔
1677
        return [caseArguments, caseString];
3,968✔
1678
    }
3,968✔
1679

1✔
1680
    /**
1✔
1681
     * Convert SQL CASE to javascript executeable code to solve case options.
1✔
1682
     * @param {any[]} args - current CASE WHEN strings.
1✔
1683
     * * args[0] - entire WHEN ... THEN ...
1✔
1684
     * * args[1] - parsed string after WHEN, before THEN
1✔
1685
     * * args[2] - parse string after THEN
1✔
1686
     * @returns {String} - js code to handle this WHEN case.
1✔
1687
     */
1✔
1688
    caseWhen(args) {
1✔
1689
        let replacement = "";
90✔
1690

90✔
1691
        if (args.length > 2) {
90✔
1692
            if (typeof args[1] === 'undefined' && typeof args[2] === 'undefined') {
90✔
1693
                replacement = `else return ${args[3]};`;
42✔
1694
            }
42✔
1695
            else {
48✔
1696
                if (this.firstCase) {
48✔
1697
                    replacement = "(() => {if (";
42✔
1698
                    this.firstCase = false;
42✔
1699
                }
42✔
1700
                else
6✔
1701
                    replacement = "else if (";
6✔
1702
                replacement += `${SqlParse.sqlCondition2JsCondition(args[1])}) return ${args[2]} ;`;
48✔
1703
            }
48✔
1704
        }
90✔
1705

90✔
1706
        return replacement;
90✔
1707
    }
90✔
1708

1✔
1709
    /**
1✔
1710
     * Finish up the javascript code to handle the select CASE.
1✔
1711
     * @param {String} func - current function being processed.  If <> 'CASE', ignore.
1✔
1712
     * @param {String} funcString - current SQL/javascript string in the process of being converted to js.
1✔
1713
     * @returns {String} - updated js code
1✔
1714
     */
1✔
1715
    caseEnd(func, funcString) {
1✔
1716
        let functionString = funcString;
3,968✔
1717

3,968✔
1718
        if (func === "CASE" && this.originalFunctionString !== "") {
3,968✔
1719
            functionString += "})();";      //  end of lambda.
42✔
1720
            functionString = this.originalFunctionString.replace(this.originalCaseStatement, functionString);
42✔
1721
        }
42✔
1722

3,968✔
1723
        return functionString;
3,968✔
1724
    }
3,968✔
1725
}
1✔
1726

1✔
1727
/** Used to create a single row from multiple rows for GROUP BY expressions. */
1✔
1728
class ConglomerateRecord {
1✔
1729
    /**
1✔
1730
     * 
1✔
1731
     * @param {TableField[]} virtualFields 
1✔
1732
     */
1✔
1733
    constructor(virtualFields) {
1✔
1734
        /** @property {TableField[]} */
55✔
1735
        this.selectVirtualFields = virtualFields;
55✔
1736
    }
55✔
1737

1✔
1738
    /**
1✔
1739
     * Compress group records to a single row by applying appropriate aggregate functions.
1✔
1740
     * @param {any[][]} groupRecords - a group of table data records to compress.
1✔
1741
     * @returns {any[]} - compressed record.
1✔
1742
     * * If column is not an aggregate function, value from first row of group records is selected. (should all be the same)
1✔
1743
     * * If column has aggregate function, that function is applied to all rows from group records.
1✔
1744
     */
1✔
1745
    squish(groupRecords) {
1✔
1746
        const row = [];
150✔
1747
        if (groupRecords.length === 0)
150✔
1748
            return row;
150✔
1749

146✔
1750
        let i = 0;
146✔
1751
        for (/** @type {TableField} */ const field of this.selectVirtualFields) {
150✔
1752
            if (field.aggregateFunction === "")
461✔
1753
                row.push(groupRecords[0][i]);
461✔
1754
            else {
318✔
1755
                row.push(ConglomerateRecord.aggregateColumn(field, groupRecords, i));
318✔
1756
            }
318✔
1757
            i++;
461✔
1758
        }
461✔
1759
        return row;
146✔
1760
    }
150✔
1761

1✔
1762
    /**
1✔
1763
     * Apply aggregate function to all rows on specified column and return result.
1✔
1764
     * @param {TableField} field - field with aggregate function
1✔
1765
     * @param {any[]} groupRecords - group of records we apply function to.
1✔
1766
     * @param {Number} columnIndex - the column index where data is read from and function is applied on.
1✔
1767
     * @returns {Number} - value of aggregate function for all group rows.
1✔
1768
     */
1✔
1769
    static aggregateColumn(field, groupRecords, columnIndex) {
1✔
1770
        let groupValue = 0;
318✔
1771
        let avgCounter = 0;
318✔
1772
        let first = true;
318✔
1773
        const distinctSet = new Set();
318✔
1774

318✔
1775
        for (const groupRow of groupRecords) {
318✔
1776
            if (groupRow[columnIndex] === 'null')
838✔
1777
                continue;
838✔
1778

535✔
1779
            let numericData = parseFloat(groupRow[columnIndex]);
535✔
1780
            numericData = (isNaN(numericData)) ? 0 : numericData;
838✔
1781

838✔
1782
            switch (field.aggregateFunction) {
838✔
1783
                case "SUM":
838✔
1784
                    groupValue += numericData;
212✔
1785
                    break;
212✔
1786
                case "COUNT":
838✔
1787
                    groupValue++;
152✔
1788
                    if (field.distinctSetting === "DISTINCT") {
152✔
1789
                        distinctSet.add(groupRow[columnIndex]);
38✔
1790
                        groupValue = distinctSet.size;
38✔
1791
                    }
38✔
1792
                    break;
152✔
1793
                case "MIN":
838✔
1794
                    groupValue = ConglomerateRecord.minCase(first, groupValue, numericData);
19✔
1795
                    break;
19✔
1796
                case "MAX":
838✔
1797
                    groupValue = ConglomerateRecord.maxCase(first, groupValue, numericData);
58✔
1798
                    break;
58✔
1799
                case "AVG":
838✔
1800
                    avgCounter++;
94✔
1801
                    groupValue += numericData;
94✔
1802
                    break;
94✔
1803
                default:
838!
1804
                    throw new Error(`Invalid aggregate function: ${field.aggregateFunction}`);
×
1805
            }
838✔
1806
            first = false;
535✔
1807
        }
535✔
1808

318✔
1809
        if (field.aggregateFunction === "AVG")
318✔
1810
            groupValue = groupValue / avgCounter;
318✔
1811

318✔
1812
        return groupValue;
318✔
1813
    }
318✔
1814

1✔
1815
    /**
1✔
1816
     * Find minimum value from group records.
1✔
1817
     * @param {Boolean} first - true if first record in set.
1✔
1818
     * @param {Number} value - cumulative data from all previous group records
1✔
1819
     * @param {Number} data - data from current group record
1✔
1820
     * @returns {Number} - minimum value from set.
1✔
1821
     */
1✔
1822
    static minCase(first, value, data) {
1✔
1823
        let groupValue = value;
19✔
1824
        if (first)
19✔
1825
            groupValue = data;
19✔
1826
        if (data < groupValue)
19✔
1827
            groupValue = data;
19✔
1828

19✔
1829
        return groupValue;
19✔
1830
    }
19✔
1831

1✔
1832
    /**
1✔
1833
     * Find max value from group records.
1✔
1834
     * @param {Boolean} first - true if first record in set.
1✔
1835
     * @param {Number} value - cumulative data from all previous group records.
1✔
1836
     * @param {Number} data - data from current group record
1✔
1837
     * @returns {Number} - max value from set.
1✔
1838
     */
1✔
1839
    static maxCase(first, value, data) {
1✔
1840
        let groupValue = value;
58✔
1841
        if (first)
58✔
1842
            groupValue = data;
58✔
1843
        if (data > groupValue)
58✔
1844
            groupValue = data;
58✔
1845

58✔
1846
        return groupValue;
58✔
1847
    }
58✔
1848
}
1✔
1849

1✔
1850
/** Fields from all tables. */
1✔
1851
class TableFields {
1✔
1852
    constructor() {
1✔
1853
        /** @property {TableField[]} */
206✔
1854
        this.allFields = [];
206✔
1855
        /** @property {Map<String, TableField>} */
206✔
1856
        this.fieldNameMap = new Map();
206✔
1857
        /** @property {Map<String, TableField>} */
206✔
1858
        this.tableColumnMap = new Map();
206✔
1859
    }
206✔
1860

1✔
1861
    /**
1✔
1862
     * Iterate through all table fields and create a list of these VirtualFields.
1✔
1863
     * @param {String} primaryTable - primary FROM table name in select.
1✔
1864
     * @param {Map<String,Table>} tableInfo - map of all loaded tables. 
1✔
1865
     */
1✔
1866
    loadVirtualFields(primaryTable, tableInfo) {
1✔
1867
        /** @type {String} */
205✔
1868
        let tableName = "";
205✔
1869
        /** @type {Table} */
205✔
1870
        let tableObject = null;
205✔
1871
        // @ts-ignore
205✔
1872
        for ([tableName, tableObject] of tableInfo.entries()) {
205✔
1873
            const validFieldNames = tableObject.getAllFieldNames();
335✔
1874

335✔
1875
            for (const field of validFieldNames) {
335✔
1876
                const tableColumn = tableObject.getFieldColumn(field);
3,545✔
1877
                if (tableColumn !== -1) {
3,545✔
1878
                    let virtualField = this.findTableField(tableName, tableColumn);
3,545✔
1879
                    if (virtualField !== null) {
3,545✔
1880
                        virtualField.addAlias(field);
1,809✔
1881
                    }
1,809✔
1882
                    else {
1,736✔
1883
                        virtualField = new TableField()
1,736✔
1884
                            .setOriginalTable(tableName)
1,736✔
1885
                            .setOriginalTableColumn(tableColumn)
1,736✔
1886
                            .addAlias(field)
1,736✔
1887
                            .setIsPrimaryTable(primaryTable.toUpperCase() === tableName.toUpperCase())
1,736✔
1888
                            .setTableInfo(tableObject);
1,736✔
1889

1,736✔
1890
                        this.allFields.push(virtualField);
1,736✔
1891
                    }
1,736✔
1892

3,545✔
1893
                    this.indexTableField(virtualField, primaryTable.toUpperCase() === tableName.toUpperCase());
3,545✔
1894
                }
3,545✔
1895
            }
3,545✔
1896
        }
335✔
1897

205✔
1898
        this.allFields.sort(TableFields.sortPrimaryFields);
205✔
1899
    }
205✔
1900

1✔
1901
    /**
1✔
1902
     * Sort function for table fields list.
1✔
1903
     * @param {TableField} fldA 
1✔
1904
     * @param {TableField} fldB 
1✔
1905
     */
1✔
1906
    static sortPrimaryFields(fldA, fldB) {
1✔
1907
        let keyA = fldA.isPrimaryTable ? 0 : 1000;
2,461✔
1908
        let keyB = fldB.isPrimaryTable ? 0 : 1000;
2,461✔
1909

2,461✔
1910
        keyA += fldA.originalTableColumn;
2,461✔
1911
        keyB += fldB.originalTableColumn;
2,461✔
1912

2,461✔
1913
        if (keyA < keyB)
2,461✔
1914
            return -1;
2,461✔
1915
        else if (keyA > keyB)
1,720✔
1916
            return 1;
1,720✔
1917
        return 0;
134✔
1918
    }
2,461✔
1919

1✔
1920
    /**
1✔
1921
     * Set up mapping to quickly find field info - by all (alias) names, by table+column.
1✔
1922
     * @param {TableField} field - field info.
1✔
1923
     * @param {Boolean} isPrimaryTable - is this a field from the SELECT FROM TABLE.
1✔
1924
     */
1✔
1925
    indexTableField(field, isPrimaryTable = false) {
1✔
1926
        for (const aliasField of field.aliasNames) {
4,312✔
1927
            const fieldInfo = this.fieldNameMap.get(aliasField.toUpperCase());
6,689✔
1928

6,689✔
1929
            if (typeof fieldInfo === 'undefined' || isPrimaryTable) {
6,689✔
1930
                this.fieldNameMap.set(aliasField.toUpperCase(), field);
4,643✔
1931
            }
4,643✔
1932
        }
6,689✔
1933

4,312✔
1934
        const key = `${field.originalTable}:${field.originalTableColumn}`;
4,312✔
1935
        if (!this.tableColumnMap.has(key))
4,312✔
1936
            this.tableColumnMap.set(key, field);
4,312✔
1937
    }
4,312✔
1938

1✔
1939
    /**
1✔
1940
     * Quickly find field info for TABLE + COLUMN NUMBER (key of map)
1✔
1941
     * @param {String} tableName - Table name to search for.
1✔
1942
     * @param {Number} tableColumn - Column number to search for.
1✔
1943
     * @returns {TableField} -located table info (null if not found).
1✔
1944
     */
1✔
1945
    findTableField(tableName, tableColumn) {
1✔
1946
        const key = `${tableName}:${tableColumn}`;
3,545✔
1947

3,545✔
1948
        if (!this.tableColumnMap.has(key)) {
3,545✔
1949
            return null;
1,736✔
1950
        }
1,736✔
1951

1,809✔
1952
        return this.tableColumnMap.get(key);
1,809✔
1953
    }
3,545✔
1954

1✔
1955
    /**
1✔
1956
     * Is this field in our map.
1✔
1957
     * @param {String} field - field name
1✔
1958
     * @returns {Boolean} - found in map if true.
1✔
1959
     */
1✔
1960
    hasField(field) {
1✔
1961
        return this.fieldNameMap.has(field.toUpperCase());
2,101✔
1962
    }
2,101✔
1963

1✔
1964
    /**
1✔
1965
     * Get field info.
1✔
1966
     * @param {String} field - table column name to find 
1✔
1967
     * @returns {TableField} - table info (undefined if not found)
1✔
1968
     */
1✔
1969
    getFieldInfo(field) {
1✔
1970
        return this.fieldNameMap.get(field.toUpperCase());
1,915✔
1971
    }
1,915✔
1972

1✔
1973
    /**
1✔
1974
     * Get table associated with field name.
1✔
1975
     * @param {String} field - field name to search for
1✔
1976
     * @returns {Table} - associated table info (undefined if not found)
1✔
1977
     */
1✔
1978
    getTableInfo(field) {
1✔
1979
        const fldInfo = this.getFieldInfo(field);
132✔
1980

132✔
1981
        return typeof fldInfo !== 'undefined' ? fldInfo.tableInfo : fldInfo;
132!
1982
    }
132✔
1983

1✔
1984
    /**
1✔
1985
     * Get column number for field.
1✔
1986
     * @param {String} field - field name
1✔
1987
     * @returns {Number} - column number in table for field (-1 if not found)
1✔
1988
     */
1✔
1989
    getFieldColumn(field) {
1✔
1990
        const fld = this.getFieldInfo(field);
132✔
1991
        if (fld !== null) {
132✔
1992
            return fld.tableColumn;
132✔
1993
        }
132✔
1994

×
1995
        return -1;
×
1996
    }
132✔
1997

1✔
1998
    /**
1✔
1999
     * Get field column number.
1✔
2000
     * @param {String} field - field name
1✔
2001
     * @returns {Number} - column number.
1✔
2002
     */
1✔
2003
    getSelectFieldColumn(field) {
1✔
2004
        let fld = this.getFieldInfo(field);
394✔
2005
        if (fld !== null && fld.selectColumn !== -1) {
394✔
2006
            return fld.selectColumn;
339✔
2007
        }
339✔
2008

54✔
2009
        for (fld of this.getSelectFields()) {
394✔
2010
            if (fld.aliasNames.indexOf(field.toUpperCase()) !== -1) {
171✔
2011
                return fld.selectColumn;
50✔
2012
            }
50✔
2013
        }
171✔
2014

4✔
2015
        return -1;
4✔
2016
    }
394✔
2017

1✔
2018
    /**
1✔
2019
     * Updates internal SELECTED (returned in data) field list.
1✔
2020
     * @param {Object} astFields - AST from SELECT
1✔
2021
     */
1✔
2022
    updateSelectFieldList(astFields) {
1✔
2023
        let i = 0;
205✔
2024
        for (const selField of astFields) {
205✔
2025
            const parsedField = this.parseAstSelectField(selField);
767✔
2026
            const columnTitle = (typeof selField.as !== 'undefined' && selField.as !== "" ? selField.as : selField.name);
767✔
2027

767✔
2028
            if (parsedField.calculatedField === null && this.hasField(parsedField.columnName)) {
767✔
2029
                let fieldInfo = this.getFieldInfo(parsedField.columnName);
615✔
2030
                if (parsedField.aggregateFunctionName !== "" || fieldInfo.selectColumn !== -1) {
615✔
2031
                    //  A new SELECT field, not from existing.
35✔
2032
                    const newFieldInfo = new TableField();
35✔
2033
                    Object.assign(newFieldInfo, fieldInfo);
35✔
2034
                    fieldInfo = newFieldInfo;
35✔
2035

35✔
2036
                    this.allFields.push(fieldInfo);
35✔
2037
                }
35✔
2038

615✔
2039
                fieldInfo
615✔
2040
                    .setAggregateFunction(parsedField.aggregateFunctionName)
615✔
2041
                    .setColumnTitle(columnTitle)
615✔
2042
                    .setColumnName(selField.name)
615✔
2043
                    .setDistinctSetting(parsedField.fieldDistinct)
615✔
2044
                    .setSelectColumn(i);
615✔
2045

615✔
2046
                this.indexTableField(fieldInfo);
615✔
2047
            }
615✔
2048
            else if (parsedField.calculatedField !== null) {
152✔
2049
                const fieldInfo = new TableField();
66✔
2050
                this.allFields.push(fieldInfo);
66✔
2051

66✔
2052
                fieldInfo
66✔
2053
                    .setColumnTitle(columnTitle)
66✔
2054
                    .setColumnName(selField.name)
66✔
2055
                    .setSelectColumn(i)
66✔
2056
                    .setCalculatedFormula(selField.name)
66✔
2057
                    .setSubQueryAst(selField.subQuery);
66✔
2058

66✔
2059
                this.indexTableField(fieldInfo);
66✔
2060
            }
66✔
2061
            else {
86✔
2062
                const fieldInfo = new TableField();
86✔
2063
                this.allFields.push(fieldInfo);
86✔
2064

86✔
2065
                fieldInfo
86✔
2066
                    .setCalculatedFormula(parsedField.columnName)
86✔
2067
                    .setAggregateFunction(parsedField.aggregateFunctionName)
86✔
2068
                    .setSelectColumn(i)
86✔
2069
                    .setColumnName(selField.name)
86✔
2070
                    .setColumnTitle(columnTitle);
86✔
2071

86✔
2072
                this.indexTableField(fieldInfo);
86✔
2073
            }
86✔
2074
            i++;
767✔
2075
        }
767✔
2076
    }
205✔
2077

1✔
2078
    /**
1✔
2079
     * Fields in GROUP BY and ORDER BY might not be in the SELECT field list.  Add a TEMP version to that list.
1✔
2080
     * @param {Object} ast - AST to search for GROUP BY and ORDER BY.
1✔
2081
     */
1✔
2082
    addReferencedColumnstoSelectFieldList(ast) {
1✔
2083
        this.addTempMissingSelectedField(ast['GROUP BY']);
205✔
2084
        this.addTempMissingSelectedField(ast['ORDER BY']);
205✔
2085
    }
205✔
2086

1✔
2087
    /**
1✔
2088
     * Add to Select field list as a temporary field for the fields in AST.
1✔
2089
     * @param {Object} astColumns - find columns mentioned not already in Select Field List
1✔
2090
     */
1✔
2091
    addTempMissingSelectedField(astColumns) {
1✔
2092
        if (typeof astColumns !== 'undefined') {
410✔
2093
            for (const order of astColumns) {
52✔
2094
                if (this.getSelectFieldColumn(order.column) === -1) {
56✔
2095
                    const fieldInfo = this.getFieldInfo(order.column);
4✔
2096

4✔
2097
                    //  A new SELECT field, not from existing.
4✔
2098
                    const newFieldInfo = new TableField();
4✔
2099
                    Object.assign(newFieldInfo, fieldInfo);
4✔
2100
                    newFieldInfo
4✔
2101
                        .setSelectColumn(this.getNextSelectColumnNumber())
4✔
2102
                        .setIsTempField(true);
4✔
2103

4✔
2104
                    this.allFields.push(newFieldInfo);
4✔
2105
                }
4✔
2106
            }
56✔
2107
        }
51✔
2108
    }
410✔
2109

1✔
2110
    /**
1✔
2111
     * Find next available column number in selected field list.
1✔
2112
     * @returns {Number} - column number
1✔
2113
     */
1✔
2114
    getNextSelectColumnNumber() {
1✔
2115
        let next = -1;
4✔
2116
        for (const fld of this.getSelectFields()) {
4✔
2117
            next = fld.selectColumn > next ? fld.selectColumn : next;
9!
2118
        }
9✔
2119

4✔
2120
        return next === -1 ? next : ++next;
4!
2121
    }
4✔
2122

1✔
2123
    /**
1✔
2124
     * Return a list of temporary column numbers in select field list.
1✔
2125
     * @returns {Number[]} - sorted list of temp column numbers.
1✔
2126
     */
1✔
2127
    getTempSelectedColumnNumbers() {
1✔
2128
        /** @type {Number[]} */
187✔
2129
        const tempCols = [];
187✔
2130
        for (const fld of this.getSelectFields()) {
187✔
2131
            if (fld.tempField) {
715✔
2132
                tempCols.push(fld.selectColumn);
4✔
2133
            }
4✔
2134
        }
715✔
2135
        tempCols.sort((a, b) => (b - a));
187✔
2136

187✔
2137
        return tempCols;
187✔
2138
    }
187✔
2139

1✔
2140
    /**
1✔
2141
     * Get a sorted list (by column number) of selected fields.
1✔
2142
     * @returns {TableField[]} - selected fields
1✔
2143
     */
1✔
2144
    getSelectFields() {
1✔
2145
        const selectedFields = this.allFields.filter((a) => a.selectColumn !== -1);
1,787✔
2146
        selectedFields.sort((a, b) => a.selectColumn - b.selectColumn);
1,787✔
2147

1,787✔
2148
        return selectedFields;
1,787✔
2149
    }
1,787✔
2150

1✔
2151
    /**
1✔
2152
     * Get SELECTED Field names sorted list of column number.
1✔
2153
     * @returns {String[]} - Table field names
1✔
2154
     */
1✔
2155
    getColumnNames() {
1✔
2156
        const columnNames = [];
2✔
2157

2✔
2158
        for (const fld of this.getSelectFields()) {
2✔
2159
            columnNames.push(fld.columnName);
4✔
2160
        }
4✔
2161

2✔
2162
        return columnNames;
2✔
2163
    }
2✔
2164

1✔
2165
    /**
1✔
2166
     * Get column titles. If alias was set, that column would be the alias, otherwise it is column name.
1✔
2167
     * @returns {String[]} - column titles
1✔
2168
     */
1✔
2169
    getColumnTitles() {
1✔
2170
        const columnTitles = [];
108✔
2171

108✔
2172
        for (const fld of this.getSelectFields()) {
108✔
2173
            if (!fld.tempField) {
488✔
2174
                columnTitles.push(fld.columnTitle);
484✔
2175
            }
484✔
2176
        }
488✔
2177

108✔
2178
        return columnTitles;
108✔
2179
    }
108✔
2180

1✔
2181
    /**
1✔
2182
     * Derived tables will cause an update to any TableField.  It updates with a new column number and new table (derived) info.
1✔
2183
     * @param {DerivedTable} derivedTable - derived table info.
1✔
2184
     */
1✔
2185
    updateDerivedTableVirtualFields(derivedTable) {
1✔
2186
        const derivedTableFields = derivedTable.tableInfo.getAllVirtualFields();
41✔
2187

41✔
2188
        let fieldNo = 0;
41✔
2189
        for (const field of derivedTableFields) {
41✔
2190
            if (this.hasField(field.fieldName)) {
552✔
2191
                const originalField = this.getFieldInfo(field.fieldName);
552✔
2192
                originalField.derivedTableColumn = fieldNo;
552✔
2193
                originalField.tableInfo = derivedTable.tableInfo;
552✔
2194
            }
552✔
2195

552✔
2196
            fieldNo++;
552✔
2197
        }
552✔
2198
    }
41✔
2199

1✔
2200
    /**
1✔
2201
     * @typedef {Object} ParsedSelectField
1✔
2202
     * @property {String} columnName
1✔
2203
     * @property {String} aggregateFunctionName
1✔
2204
     * @property {Object} calculatedField
1✔
2205
     * @property {String} fieldDistinct
1✔
2206
     */
1✔
2207

1✔
2208
    /**
1✔
2209
     * Parse SELECT field in AST (may include functions or calculations)
1✔
2210
     * @param {Object} selField 
1✔
2211
     * @returns {ParsedSelectField}
1✔
2212
     */
1✔
2213
    parseAstSelectField(selField) {
1✔
2214
        let columnName = selField.name;
767✔
2215
        let aggregateFunctionName = "";
767✔
2216
        let fieldDistinct = "";
767✔
2217
        const calculatedField = (typeof selField.terms === 'undefined') ? null : selField.terms;
767✔
2218

767✔
2219
        if (calculatedField === null && !this.hasField(columnName)) {
767✔
2220
            const functionNameRegex = /^\w+\s*(?=\()/;
118✔
2221
            let matches = columnName.match(functionNameRegex)
118✔
2222
            if (matches !== null && matches.length > 0)
118✔
2223
                aggregateFunctionName = matches[0].trim();
118✔
2224

118✔
2225
            matches = SelectTables.parseForFunctions(columnName, aggregateFunctionName);
118✔
2226
            if (matches !== null && matches.length > 1) {
118✔
2227
                columnName = matches[1];
110✔
2228

110✔
2229
                // e.g.  select count(distinct field)    OR   select count(all field)
110✔
2230
                [columnName, fieldDistinct] = TableFields.getSelectCountModifiers(columnName);
110✔
2231
            }
110✔
2232
        }
118✔
2233

767✔
2234
        return {columnName, aggregateFunctionName, calculatedField, fieldDistinct};
767✔
2235
    }
767✔
2236

1✔
2237
    /**
1✔
2238
     * Parse for any SELECT COUNT modifiers like 'DISTINCT' or 'ALL'.
1✔
2239
     * @param {String} originalColumnName - column (e.g. 'distinct customer_id')
1✔
2240
     * @returns {String[]} - [0] - parsed column name, [1] - count modifier
1✔
2241
     */
1✔
2242
    static getSelectCountModifiers(originalColumnName) {
1✔
2243
        let fieldDistinct = "";
110✔
2244
        let columnName = originalColumnName;
110✔
2245

110✔
2246
        //  e.g.  count(distinct field)
110✔
2247
        const distinctParts = columnName.split(" ");
110✔
2248
        if (distinctParts.length > 1) {
110✔
2249
            const distinctModifiers = ["DISTINCT", "ALL"];
59✔
2250
            if (distinctModifiers.includes(distinctParts[0].toUpperCase())) {
59✔
2251
                fieldDistinct = distinctParts[0].toUpperCase();
6✔
2252
                columnName = distinctParts[1];
6✔
2253
            }
6✔
2254
        }
59✔
2255

110✔
2256
        return [columnName, fieldDistinct];
110✔
2257
    }
110✔
2258

1✔
2259
    /**
1✔
2260
     * Counts the number of conglomerate field functions in SELECT field list.
1✔
2261
     * @returns {Number} - Number of conglomerate functions.
1✔
2262
     */
1✔
2263
    getConglomerateFieldCount() {
1✔
2264
        let count = 0;
162✔
2265
        for (/** @type {TableField} */ const field of this.getSelectFields()) {
162✔
2266
            if (field.aggregateFunction !== "")
623✔
2267
                count++;
623✔
2268
        }
623✔
2269

162✔
2270
        return count;
162✔
2271
    }
162✔
2272
}
1✔
2273

1✔
2274
/** Table column information. */
1✔
2275
class TableField {
1✔
2276
    constructor() {
1✔
2277
        /** @property {String} */
1,927✔
2278
        this.originalTable = "";
1,927✔
2279
        /** @property {Number} */
1,927✔
2280
        this.originalTableColumn = -1;
1,927✔
2281
        /** @property {String[]} */
1,927✔
2282
        this.aliasNames = [];
1,927✔
2283
        /** @property {String} */
1,927✔
2284
        this.fieldName = "";
1,927✔
2285
        /** @property {Number} */
1,927✔
2286
        this.derivedTableColumn = -1;
1,927✔
2287
        /** @property {Number} */
1,927✔
2288
        this.selectColumn = -1;
1,927✔
2289
        /** @property {Boolean} */
1,927✔
2290
        this.tempField = false;
1,927✔
2291
        /** @property {String} */
1,927✔
2292
        this.calculatedFormula = "";
1,927✔
2293
        /** @property {String} */
1,927✔
2294
        this.aggregateFunction = "";
1,927✔
2295
        /** @property {String} */
1,927✔
2296
        this.columnTitle = "";
1,927✔
2297
        /** @property {String} */
1,927✔
2298
        this.columnName = "";
1,927✔
2299
        /** @property {String} */
1,927✔
2300
        this.distinctSetting = "";
1,927✔
2301
        /** @property {Object} */
1,927✔
2302
        this.subQueryAst = null;
1,927✔
2303
        /** @property {Boolean} */
1,927✔
2304
        this._isPrimaryTable = false;
1,927✔
2305
        /** @property {Table} */
1,927✔
2306
        this.tableInfo = null;
1,927✔
2307
    }
1,927✔
2308

1✔
2309
    /**
1✔
2310
     * Get field column number.
1✔
2311
     * @returns {Number} - column number
1✔
2312
     */
1✔
2313
    get tableColumn() {
1✔
2314
        return this.derivedTableColumn === -1 ? this.originalTableColumn : this.derivedTableColumn;
179✔
2315
    }
179✔
2316

1✔
2317
    /**
1✔
2318
     * Original table name before any derived table updates.
1✔
2319
     * @param {String} table - original table name
1✔
2320
     * @returns {TableField}
1✔
2321
     */
1✔
2322
    setOriginalTable(table) {
1✔
2323
        this.originalTable = table.trim().toUpperCase();
1,736✔
2324
        return this;
1,736✔
2325
    }
1,736✔
2326

1✔
2327
    /**
1✔
2328
     * Column name found in column title row.
1✔
2329
     * @param {Number} column 
1✔
2330
     * @returns {TableField}
1✔
2331
     */
1✔
2332
    setOriginalTableColumn(column) {
1✔
2333
        this.originalTableColumn = column;
1,736✔
2334
        return this;
1,736✔
2335
    }
1,736✔
2336

1✔
2337
    /**
1✔
2338
     * Alias name assigned to field in select statement.
1✔
2339
     * @param {String} columnAlias - alias name
1✔
2340
     * @returns {TableField}
1✔
2341
     */
1✔
2342
    addAlias(columnAlias) {
1✔
2343
        const alias = columnAlias.trim().toUpperCase();
3,545✔
2344
        if (this.fieldName === "" || alias.indexOf(".") !== -1) {
3,545✔
2345
            this.fieldName = alias;
3,545✔
2346
        }
3,545✔
2347

3,545✔
2348
        if (this.aliasNames.indexOf(alias) === -1) {
3,545✔
2349
            this.aliasNames.push(alias);
3,545✔
2350
        }
3,545✔
2351

3,545✔
2352
        return this;
3,545✔
2353
    }
3,545✔
2354

1✔
2355
    /**
1✔
2356
     * Set column number in table data for field.
1✔
2357
     * @param {Number} column - column number.
1✔
2358
     * @returns {TableField}
1✔
2359
     */
1✔
2360
    setSelectColumn(column) {
1✔
2361
        this.selectColumn = column;
771✔
2362

771✔
2363
        return this;
771✔
2364
    }
771✔
2365

1✔
2366
    /**
1✔
2367
     * Fields referenced BUT not in final output.
1✔
2368
     * @param {Boolean} value 
1✔
2369
     * @returns {TableField}
1✔
2370
     */
1✔
2371
    setIsTempField(value) {
1✔
2372
        this.tempField = value;
4✔
2373
        return this;
4✔
2374
    }
4✔
2375

1✔
2376
    /**
1✔
2377
     * Aggregate function number used (e.g. 'SUM')
1✔
2378
     * @param {String} value - aggregate function name or ''
1✔
2379
     * @returns {TableField}
1✔
2380
     */
1✔
2381
    setAggregateFunction(value) {
1✔
2382
        this.aggregateFunction = value.toUpperCase();
701✔
2383
        return this;
701✔
2384
    }
701✔
2385

1✔
2386
    /**
1✔
2387
     * Calculated formula for field (e.g. 'CASE WHEN QUANTITY >= 100 THEN 1 ELSE 0 END')
1✔
2388
     * @param {String} value 
1✔
2389
     * @returns {TableField}
1✔
2390
     */
1✔
2391
    setCalculatedFormula(value) {
1✔
2392
        this.calculatedFormula = value;
152✔
2393
        return this;
152✔
2394
    }
152✔
2395

1✔
2396
    /**
1✔
2397
     * The AST from just the subquery in the SELECT.
1✔
2398
     * @param {Object} ast - subquery ast.
1✔
2399
     * @returns {TableField}
1✔
2400
     */
1✔
2401
    setSubQueryAst(ast) {
1✔
2402
        this.subQueryAst = ast;
66✔
2403
        return this;
66✔
2404
    }
66✔
2405

1✔
2406
    /**
1✔
2407
     * Set column TITLE.  If an alias is available, that is used - otherwise it is column name.
1✔
2408
     * @param {String} columnTitle - column title used in output
1✔
2409
     * @returns {TableField}
1✔
2410
     */
1✔
2411
    setColumnTitle(columnTitle) {
1✔
2412
        this.columnTitle = columnTitle;
767✔
2413
        return this;
767✔
2414
    }
767✔
2415

1✔
2416
    /**
1✔
2417
     * Set the columnname.
1✔
2418
     * @param {String} columnName 
1✔
2419
     * @returns {TableField}
1✔
2420
     */
1✔
2421
    setColumnName(columnName) {
1✔
2422
        this.columnName = columnName;
767✔
2423
        return this;
767✔
2424
    }
767✔
2425

1✔
2426
    /**
1✔
2427
     * Set any count modified like 'DISTINCT' or 'ALL'.
1✔
2428
     * @param {String} distinctSetting 
1✔
2429
     * @returns 
1✔
2430
     */
1✔
2431
    setDistinctSetting(distinctSetting) {
1✔
2432
        this.distinctSetting = distinctSetting;
615✔
2433
        return this
615✔
2434
    }
615✔
2435

1✔
2436
    /**
1✔
2437
     * Set if this field belongs to primary table (i.e. select * from table), rather than a joined tabled.
1✔
2438
     * @param {Boolean} isPrimary - true if from primary table.
1✔
2439
     * @returns {TableField}
1✔
2440
     */
1✔
2441
    setIsPrimaryTable(isPrimary) {
1✔
2442
        this._isPrimaryTable = isPrimary;
1,736✔
2443
        return this;
1,736✔
2444
    }
1,736✔
2445

1✔
2446
    /**
1✔
2447
     * Is this field in the primary table.
1✔
2448
     * @returns {Boolean}
1✔
2449
     */
1✔
2450
    get isPrimaryTable() {
1✔
2451
        return this._isPrimaryTable;
4,922✔
2452
    }
4,922✔
2453

1✔
2454
    /**
1✔
2455
     * Link this field to the table info.
1✔
2456
     * @param {Table} tableInfo 
1✔
2457
     * @returns {TableField}
1✔
2458
     */
1✔
2459
    setTableInfo(tableInfo) {
1✔
2460
        this.tableInfo = tableInfo;
1,736✔
2461
        return this;
1,736✔
2462
    }
1,736✔
2463

1✔
2464
    /**
1✔
2465
     * Retrieve field data for tableRow
1✔
2466
     * @param {Number} tableRow - row to read data from
1✔
2467
     * @returns {any} - data
1✔
2468
     */
1✔
2469
    getData(tableRow) {
1✔
2470
        const columnNumber = this.derivedTableColumn === -1 ? this.originalTableColumn : this.derivedTableColumn;
11,344✔
2471
        if (tableRow < 0 || columnNumber < 0)
11,344✔
2472
            return "";
11,344!
2473

11,344✔
2474
        return this.tableInfo.tableData[tableRow][columnNumber];
11,344✔
2475
    }
11,344✔
2476

1✔
2477
    /**
1✔
2478
     * Search through list of fields and return a list of those that include the table name (e.g. TABLE.COLUMN vs COLUMN)
1✔
2479
     * @param {TableField[]} masterFields 
1✔
2480
     * @returns {String[]}
1✔
2481
     */
1✔
2482
    static getAllExtendedAliasNames(masterFields) {
1✔
2483
        const concatFields = [];
4✔
2484
        for (const vField of masterFields) {
4✔
2485
            for (const aliasName of vField.aliasNames) {
36✔
2486
                if (aliasName.indexOf(".") !== -1) {
72✔
2487
                    concatFields.push(aliasName);
36✔
2488
                }
36✔
2489
            }
72✔
2490
        }
36✔
2491

4✔
2492
        return concatFields;
4✔
2493
    }
4✔
2494
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc