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

demmings / gsSQL / 4027440288

pending completion
4027440288

push

github

cdemmigs
#22.  linter.

1254 of 1328 branches covered (94.43%)

Branch coverage included in aggregate %.

9725 of 10121 relevant lines covered (96.09%)

505.61 hits per line

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

98.53
/src/Sql.js
1
//  Remove comments for testing in NODE
1✔
2
/*  *** DEBUG START ***
1✔
3
export { Sql, gsSQL, parseTableSettings, BindData };
1✔
4
import { Table } from './Table.js';
1✔
5
import { TableData } from './TableData.js';
1✔
6
import { SqlParse } from './SimpleParser.js';
1✔
7
import { SelectTables } from './Views.js';
1✔
8

1✔
9
class Logger {
1✔
10
    static log(msg) {
1✔
11
        console.log(msg);
27✔
12
    }
27✔
13
}
1✔
14
//  *** DEBUG END  ***/
1✔
15

1✔
16
/**
1✔
17
 * @description
1✔
18
 * **CUSTOM FUNCTION**  
1✔
19
 * * Available as a custom function within your sheet.
1✔
20
 * * Query any sheet range using standard SQL SELECT syntax.
1✔
21
 * ### Parameters.
1✔
22
 * * Parameter 1.  SELECT statement.  All regular syntax is supported including JOIN. 
1✔
23
 *   * note i)  Bind variables (?) are replaced by bind data specified later.
1✔
24
 *   * note ii)  PIVOT field supported.  Similar to QUERY. e.g.  "SELECT date, sum(quantity) from sales group by date pivot customer_id".
1✔
25
 *   * note iii) If parm 2 not used and sheet name contains a space, use single quotes around table name.
1✔
26
 * * Parameter 2. (optional. referenced tables assumed to be SHEET NAME with column titles).  Define all tables referenced in SELECT. This is a DOUBLE ARRAY and is done using the curly bracket {{a,b,c}; {a,b,c}} syntax.
1✔
27
 *   * a)  table name - the table name referenced in SELECT for indicated range.
1✔
28
 *   * b)  sheet range - (optional) either NAMED RANGE, A1 notation range, SHEET NAME or empty (table name used as sheet name).  This input is a string.  The first row of each range MUST be unique column titles.
1✔
29
 *   * c)  cache seconds - (optional) time loaded range held in cache.  default=60.   
1✔
30
 *   * d)  has column title - (optional) first row of data is a title (for field name).  default=true 
1✔
31
 * * Parameter 3. (optional) Output result column title (true/false). default=true.   
1✔
32
 * * Parameter 4... (optional) Bind variables.  List as many as required to match '?' in SELECT statement.
1✔
33
 * <br>
1✔
34
 * * **Example** use inside Google Sheet Cell.
1✔
35
 * ```
1✔
36
 * =gsSQL("select title, (select count(*)  from Booksales where books.id = BookSales.book_id) as 'Quantity Sold' from books", {{"booksales","booksales", 60};{"books", "books", 60}})
1✔
37
 * ```
1✔
38
 * @param {String} statement - SQL (e.g.:  'select * from expenses')
1✔
39
 * @param {any[][]} tableArr - {{"tableName", "sheetRange", cacheSeconds, hasColumnTitle}; {"name","range",cache,true};...}"
1✔
40
 * @param {Boolean} columnTitle - TRUE will add column title to output (default=TRUE)
1✔
41
 * @param {...any} bindings - Bind variables to match '?' in SQL statement.
1✔
42
 * @returns {any[][]} - Double array of selected data.  First index ROW, Second index COLUMN.
1✔
43
 * @customfunction
1✔
44
 */
1✔
45
function gsSQL(statement, tableArr = [], columnTitle = true, ...bindings) {     //  skipcq: JS-0128
1✔
46
    const tableList = parseTableSettings(tableArr, statement);
1✔
47

1✔
48
    Logger.log(`gsSQL: tableList=${tableList}.  Statement=${statement}. List Len=${tableList.length}`);
1✔
49

1✔
50
    const sqlCmd = new Sql().enableColumnTitle(columnTitle);
1✔
51
    for (const bind of bindings) {
1!
52
        sqlCmd.addBindParameter(bind);
×
53
    }
×
54
    for (const tableDef of tableList) {
1✔
55
        sqlCmd.addTableData(tableDef[0], tableDef[1], tableDef[2], tableDef[3]);
1✔
56
    }
1✔
57
    return sqlCmd.execute(statement);
1✔
58
}
1✔
59

1✔
60
/**
1✔
61
 * 
1✔
62
 * @param {any[][]} tableArr - Referenced Table list.  This is normally the second parameter in gsSQL() custom function.  
1✔
63
 * It is a double array with first index for TABLE, and the second index are settings in the table. 
1✔
64
 * The setting index for each table is as follows:
1✔
65
 * * 0 - Table Name.
1✔
66
 * * 1 - Sheet Range.
1✔
67
 * * 2 - Cache seconds.
1✔
68
 * * 3 - First row contains title (for field name)
1✔
69
 * @param {String} statement - SQL SELECT statement.  If no data specified in 'tableArr', the SELECT is 
1✔
70
 * parsed and each referenced table is assumed to be a TAB name on the sheet.
1✔
71
 * @param {Boolean} randomOrder - Returned table list is randomized.
1✔
72
 * @returns {any[][]} - Data from 'tableArr' PLUS any extracted tables referenced from SELECT statement.
1✔
73
 * It is a double array with first index for TABLE, and the second index are settings in the table. 
1✔
74
 * The setting index for each table is as follows:
1✔
75
 * * 0 - Table Name.
1✔
76
 * * 1 - Sheet Range.
1✔
77
 * * 2 - Cache seconds.
1✔
78
 * * 3 - First row contains title (for field name)
1✔
79
 */
1✔
80
function parseTableSettings(tableArr, statement = "", randomOrder = true) {
14✔
81
    let tableList = [];
14✔
82
    let referencedTableSettings = tableArr;
14✔
83

14✔
84
    //  Get table names from the SELECT statement when no table range info is given.
14✔
85
    if (tableArr.length === 0 && statement !== "") {
14✔
86
        referencedTableSettings = Sql.getReferencedTableNames(statement);
11✔
87
    }
11✔
88

14✔
89
    if (referencedTableSettings.length === 0) {
14!
90
        throw new Error('Missing table definition {{"name","range",cache};{...}}');
×
91
    }
×
92

14✔
93
    Logger.log(`tableArr = ${referencedTableSettings}`);
14✔
94
    for (/** @type {any[]} */ const table of referencedTableSettings) {
14✔
95
        if (table.length === 1)
31✔
96
            table.push(table[0]);   // if NO RANGE, assumes table name is sheet name.
31✔
97
        if (table.length === 2)
31✔
98
            table.push(60);      //  default 0 second cache.
31✔
99
        if (table.length === 3)
31✔
100
            table.push(true);    //  default HAS column title row.
31✔
101
        if (table[1] === "")
31✔
102
            table[1] = table[0];    //  If empty range, assumes TABLE NAME is the SHEET NAME and loads entire sheet.
31!
103
        if (table.length !== 4)
31✔
104
            throw new Error("Invalid table definition [name,range,cache,hasTitle]");
31✔
105

30✔
106
        tableList.push(table);
30✔
107
    }
30✔
108

13✔
109
    //  If called at the same time, loading similar tables in similar order - all processes
13✔
110
    //  just wait for table - but if loaded in different order, each process could be loading something.
13✔
111
    if (randomOrder)
13✔
112
        tableList = tableList.sort(() => Math.random() - 0.5);
14✔
113

13✔
114
    return tableList;
13✔
115
}
14✔
116

1✔
117
/** Perform SQL SELECT using this class. */
1✔
118
class Sql {
1✔
119
    constructor() {
1✔
120
        /** @property {Map<String,Table>} - Map of referenced tables.*/
307✔
121
        this.tables = new Map();
307✔
122
        /** @property {Boolean} - Are column tables to be ouptout? */
307✔
123
        this.columnTitle = false;
307✔
124
        /** @property {BindData} - List of BIND data linked to '?' in statement. */
307✔
125
        this.bindData = new BindData();
307✔
126
        /** @property {String} - derived table name to output in column title replacing source table name. */
307✔
127
        this.columnTableNameReplacement = null;
307✔
128
    }
307✔
129

1✔
130
    /**
1✔
131
     * Add data for each referenced table in SELECT, before EXECUTE().
1✔
132
     * @param {String} tableName - Name of table referenced in SELECT.
1✔
133
     * @param {any} tableData - Either double array or a named range.
1✔
134
     * @param {Number} cacheSeconds - How long should loaded data be cached (default=0)
1✔
135
     * @param {Boolean} hasColumnTitle - Is first data row the column title?
1✔
136
     * @returns {Sql}
1✔
137
     */
1✔
138
    addTableData(tableName, tableData, cacheSeconds = 0, hasColumnTitle = true) {
1✔
139
        let tableInfo = null;
261✔
140

261✔
141
        if (Array.isArray(tableData)) {
261✔
142
            tableInfo = new Table(tableName)
254✔
143
                .setHasColumnTitle(hasColumnTitle)
254✔
144
                .loadArrayData(tableData);
254✔
145
        }
254✔
146
        else {
7✔
147
            tableInfo = new Table(tableName)
7✔
148
                .setHasColumnTitle(hasColumnTitle)
7✔
149
                .loadNamedRangeData(tableData, cacheSeconds);
7✔
150
        }
7✔
151

259✔
152
        this.tables.set(tableName.toUpperCase(), tableInfo);
259✔
153

259✔
154
        return this;
259✔
155
    }
261✔
156

1✔
157
    /**
1✔
158
     * Copies the data from an external tableMap to this instance.  
1✔
159
     * It copies a reference to outside array data only.  
1✔
160
     * The schema would need to be re-loaded.
1✔
161
     * @param {Map<String,Table>} tableMap 
1✔
162
     */
1✔
163
    copyTableData(tableMap) {
1✔
164
        // @ts-ignore
15✔
165
        for (const tableName of tableMap.keys()) {
15✔
166
            const tableInfo = tableMap.get(tableName);
33✔
167
            this.addTableData(tableName, tableInfo.tableData);
33✔
168
        }
33✔
169

15✔
170
        return this;
15✔
171
    }
15✔
172

1✔
173
    /**
1✔
174
     * Include column headers in return data.
1✔
175
     * @param {Boolean} value - true will return column names in first row of return data.
1✔
176
     * @returns {Sql}
1✔
177
     */
1✔
178
    enableColumnTitle(value) {
1✔
179
        this.columnTitle = value;
160✔
180
        return this;
160✔
181
    }
160✔
182

1✔
183
    /**
1✔
184
     * Derived table data that requires the ALIAS table name in column title.
1✔
185
     * @param {String} replacementTableName - derived table name to replace original table name.  To disable, set to null.
1✔
186
     * @returns {Sql}
1✔
187
     */
1✔
188
    replaceColumnTableNameWith(replacementTableName){
1✔
189
        this.columnTableNameReplacement = replacementTableName;
10✔
190
        return this;
10✔
191
    }
10✔
192

1✔
193
    /**
1✔
194
     * Query if this instance of Sql() will generate column titles.
1✔
195
     * @returns {Boolean}
1✔
196
     */
1✔
197
    areColumnTitlesOutput() {
1✔
198
        return this.columnTitle;
145✔
199
    }
145✔
200

1✔
201
    /**
1✔
202
     * Add a bind data value.  Must be added in order.  If bind data is a named range, use addBindNamedRangeParameter().
1✔
203
     * @param {any} value - literal data. 
1✔
204
     * @returns {Sql}
1✔
205
     */
1✔
206
    addBindParameter(value) {
1✔
207
        this.bindData.add(value);
40✔
208
        return this;
40✔
209
    }
40✔
210

1✔
211
    /**
1✔
212
     * List of bind data added so far.
1✔
213
     * @returns {any[]}
1✔
214
     */
1✔
215
    getBindData() {
1✔
216
        return this.bindData.getBindDataList();
145✔
217
    }
145✔
218

1✔
219
    /**
1✔
220
     * The BIND data is a sheet named range that will be read and used for bind data.
1✔
221
     * @param {String} value - Sheets Named Range for SINGLE CELL only.
1✔
222
     * @returns {Sql}
1✔
223
     */
1✔
224
    addBindNamedRangeParameter(value) {
1✔
225
        const namedValue = TableData.getValueCached(value, 30);
12✔
226
        this.bindData.add(namedValue);
12✔
227
        Logger.log(`BIND=${value} = ${namedValue}`);
12✔
228
        return this;
12✔
229
    }
12✔
230

1✔
231
    /**
1✔
232
     * Set all bind data at once using array.
1✔
233
     * @param {BindData} value - Bind data.
1✔
234
     * @returns {Sql}
1✔
235
     */
1✔
236
    setBindValues(value) {
1✔
237
        this.bindData = value;
149✔
238
        return this;
149✔
239
    }
149✔
240

1✔
241
    /**
1✔
242
     * Clears existing BIND data so Sql() instance can be used again with new bind parameters.
1✔
243
     * @returns {Sql}
1✔
244
     */
1✔
245
    clearBindParameters() {
1✔
246
        this.bindData.clear();
1✔
247
        return this;
1✔
248
    }
1✔
249

1✔
250
    /**
1✔
251
    * Parse SQL SELECT statement, performs SQL query and returns data ready for custom function return.
1✔
252
    * <br>Execute() can be called multiple times for different SELECT statements, provided that all required
1✔
253
    * table data was loaded in the constructor.  
1✔
254
    * Methods that would be used PRIOR to execute are:
1✔
255
    * <br>**enableColumnTitle()** - turn on/off column title in output
1✔
256
    * <br>**addBindParameter()** - If bind data is needed in select.  e.g. "select * from table where id = ?"
1✔
257
    * <br>**addTableData()** - At least ONE table needs to be added prior to execute. This tells **execute** where to find the data.
1✔
258
    * <br>**Example SELECT and RETURN Data**
1✔
259
    * ```js
1✔
260
    *   let stmt = "SELECT books.id, books.title, books.author_id " +
1✔
261
    *        "FROM books " +
1✔
262
    *        "WHERE books.author_id IN ('11','12') " +
1✔
263
    *        "ORDER BY books.title";
1✔
264
    *
1✔
265
    *    let data = new Sql()
1✔
266
    *        .addTableData("books", this.bookTable())
1✔
267
    *        .enableColumnTitle(true)
1✔
268
    *        .execute(stmt);
1✔
269
    * 
1✔
270
    *    Logger.log(data);
1✔
271
    * 
1✔
272
    * [["books.id", "books.title", "books.author_id"],
1✔
273
    *    ["4", "Dream Your Life", "11"],
1✔
274
    *    ["8", "My Last Book", "11"],
1✔
275
    *    ["5", "Oranges", "12"],
1✔
276
    *    ["1", "Time to Grow Up!", "11"]]
1✔
277
    * ```
1✔
278
    * @param {any} statement - SELECT statement as STRING or AST of SELECT statement.
1✔
279
    * @returns {any[][]} - Double array where first index is ROW and second index is COLUMN.
1✔
280
    */
1✔
281
    execute(statement) {
1✔
282
        let sqlData = [];
309✔
283

309✔
284
        if (typeof statement === 'string') {
309✔
285
            this.ast = SqlParse.sql2ast(statement);
146✔
286
        }
146✔
287
        else {
163✔
288
            this.ast = statement;
163✔
289
        }
163✔
290

308✔
291
        //  "SELECT * from (select a,b,c from table) as derivedtable"
308✔
292
        //  Sub query data is loaded and given the name 'derivedtable'
308✔
293
        //  The AST.FROM is updated from the sub-query to the new derived table name. 
308✔
294
        this.selectFromSubQuery();
308✔
295

308✔
296
        Sql.setTableAlias(this.tables, this.ast);
308✔
297
        Sql.loadSchema(this.tables);
308✔
298

308✔
299
        if (typeof this.ast.SELECT !== 'undefined')
308✔
300
            sqlData = this.select(this.ast);
309✔
301
        else
1✔
302
            throw new Error("Only SELECT statements are supported.");
1✔
303

286✔
304
        return sqlData;
286✔
305
    }
309✔
306

1✔
307
    /**
1✔
308
     * Updates 'tables' with table column information.
1✔
309
     * @param {Map<String,Table>} tables 
1✔
310
     */
1✔
311
    static loadSchema(tables) {
1✔
312
        // @ts-ignore
307✔
313
        for (const table of tables.keys()) {
307✔
314
            const tableInfo = tables.get(table.toUpperCase());
459✔
315
            tableInfo.loadSchema();
459✔
316
        }
459✔
317
    }
307✔
318

1✔
319
    /**
1✔
320
     * Updates 'tables' with associated table ALIAS name found in ast.
1✔
321
     * @param {Map<String,Table>} tables 
1✔
322
     * @param {Object} ast 
1✔
323
     */
1✔
324
    static setTableAlias(tables, ast) {
1✔
325
        // @ts-ignore
308✔
326
        for (const table of tables.keys()) {
308✔
327
            const tableAlias = Sql.getTableAlias(table, ast);
460✔
328
            const tableInfo = tables.get(table.toUpperCase());
460✔
329
            tableInfo.setTableAlias(tableAlias);
460✔
330
        }
460✔
331
    }
308✔
332

1✔
333
    /**
1✔
334
     * Sets all tables referenced SELECT.
1✔
335
    * @param {Map<String,Table>} mapOfTables - Map of referenced tables indexed by TABLE name.
1✔
336
    */
1✔
337
    setTables(mapOfTables) {
1✔
338
        this.tables = mapOfTables;
146✔
339
        return this;
146✔
340
    }
146✔
341

1✔
342
    /**
1✔
343
     * Returns a map of all tables configured for this SELECT.
1✔
344
     * @returns {Map<String,Table>} - Map of referenced tables indexed by TABLE name.
1✔
345
     */
1✔
346
    getTables() {
1✔
347
        return this.tables;
160✔
348
    }
160✔
349

1✔
350
    /**
1✔
351
    * Find table alias name (if any) for input actual table name.
1✔
352
    * @param {String} tableName - Actual table name.
1✔
353
    * @param {Object} ast - Abstract Syntax Tree for SQL.
1✔
354
    * @returns {String} - Table alias.  Empty string if not found.
1✔
355
    */
1✔
356
    static getTableAlias(tableName, ast) {
1✔
357
        let tableAlias = "";
644✔
358
        const ucTableName = tableName.toUpperCase();
644✔
359

644✔
360
        tableAlias = Sql.getTableAliasFromJoin(tableAlias, ucTableName, ast);
644✔
361
        tableAlias = Sql.getTableAliasUnion(tableAlias, ucTableName, ast);
644✔
362
        tableAlias = Sql.getTableAliasWhereIn(tableAlias, ucTableName, ast);
644✔
363
        tableAlias = Sql.getTableAliasWhereTerms(tableAlias, ucTableName, ast);
644✔
364

644✔
365
        return tableAlias;
644✔
366
    }
644✔
367

1✔
368
    /**
1✔
369
     * Modifies AST when FROM is a sub-query rather than a table name.
1✔
370
     */
1✔
371
    selectFromSubQuery() {
1✔
372
        if (typeof this.ast.FROM !== 'undefined' && typeof this.ast.FROM.SELECT !== 'undefined') {
308✔
373
            const data = new Sql()
10✔
374
                .setTables(this.tables)
10✔
375
                .enableColumnTitle(true)
10✔
376
                .replaceColumnTableNameWith(this.ast.FROM.table)
10✔
377
                .execute(this.ast.FROM);
10✔
378

10✔
379
            if (typeof this.ast.FROM.table !== 'undefined') {
10✔
380
                this.addTableData(this.ast.FROM.table, data);
9✔
381
            }
9✔
382

10✔
383
            if (this.ast.FROM.table === '') {
10!
384
                throw new Error("Every derived table must have its own alias");
×
385
            }
×
386

10✔
387
            this.ast.FROM.as = '';
10✔
388
        }
10✔
389
    }
308✔
390

1✔
391
    /**
1✔
392
     * Searches the FROM and JOIN components of a SELECT to find the table alias.
1✔
393
     * @param {String} tableAlias - Default alias name
1✔
394
     * @param {String} tableName - table name to search for.
1✔
395
     * @param {Object} ast - Abstract Syntax Tree to search
1✔
396
     * @returns {String} - Table alias name.
1✔
397
     */
1✔
398
    static getTableAliasFromJoin(tableAlias, tableName, ast) {
1✔
399
        const astTableBlocks = ['FROM', 'JOIN'];
644✔
400
        let aliasNameFound = tableAlias;
644✔
401

644✔
402
        let i = 0;
644✔
403
        while (aliasNameFound === "" && i < astTableBlocks.length) {
644✔
404
            aliasNameFound = Sql.locateAstTableAlias(tableName, ast, astTableBlocks[i]);
1,255✔
405
            i++;
1,255✔
406
        }
1,255✔
407

643✔
408
        return aliasNameFound;
643✔
409
    }
644✔
410

1✔
411
    /**
1✔
412
     * Searches the UNION portion of the SELECT to locate the table alias.
1✔
413
     * @param {String} tableAlias - default table alias.
1✔
414
     * @param {String} tableName - table name to search for.
1✔
415
     * @param {Object} ast - Abstract Syntax Tree to search
1✔
416
     * @returns {String} - table alias
1✔
417
     */
1✔
418
    static getTableAliasUnion(tableAlias, tableName, ast) {
1✔
419
        const astRecursiveTableBlocks = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
643✔
420
        let extractedAlias = tableAlias;
643✔
421

643✔
422
        let i = 0;
643✔
423
        while (extractedAlias === "" && i < astRecursiveTableBlocks.length) {
643✔
424
            if (typeof ast[astRecursiveTableBlocks[i]] !== 'undefined') {
2,413✔
425
                for (const unionAst of ast[astRecursiveTableBlocks[i]]) {
23✔
426
                    extractedAlias = Sql.getTableAlias(tableName, unionAst);
29✔
427

29✔
428
                    if (extractedAlias !== "")
29✔
429
                        break;
29✔
430
                }
29✔
431
            }
23✔
432
            i++;
2,413✔
433
        }
2,413✔
434

643✔
435
        return extractedAlias;
643✔
436
    }
643✔
437

1✔
438
    /**
1✔
439
     * Search WHERE IN component of SELECT to find table alias.
1✔
440
     * @param {String} tableAlias - default table alias
1✔
441
     * @param {String} tableName - table name to search for
1✔
442
     * @param {Object} ast - Abstract Syntax Tree to search
1✔
443
     * @returns {String} - table alias
1✔
444
     */
1✔
445
    static getTableAliasWhereIn(tableAlias, tableName, ast) {
1✔
446
        let extractedAlias = tableAlias;
643✔
447
        if (tableAlias === "" && typeof ast.WHERE !== 'undefined' && ast.WHERE.operator === "IN") {
643✔
448
            extractedAlias = Sql.getTableAlias(tableName, ast.WHERE.right);
29✔
449
        }
29✔
450

643✔
451
        if (extractedAlias === "" && ast.operator === "IN") {
643✔
452
            extractedAlias = Sql.getTableAlias(tableName, ast.right);
11✔
453
        }
11✔
454

643✔
455
        return extractedAlias;
643✔
456
    }
643✔
457

1✔
458
    /**
1✔
459
     * Search WHERE terms of SELECT to find table alias.
1✔
460
     * @param {String} tableAlias - default table alias
1✔
461
     * @param {String} tableName  - table name to search for.
1✔
462
     * @param {Object} ast - Abstract Syntax Tree to search.
1✔
463
     * @returns {String} - table alias
1✔
464
     */
1✔
465
    static getTableAliasWhereTerms(tableAlias, tableName, ast) {
1✔
466
        let extractedTableAlias = tableAlias;
643✔
467
        if (tableAlias === "" && typeof ast.WHERE !== 'undefined' && typeof ast.WHERE.terms !== 'undefined') {
643✔
468
            for (const term of ast.WHERE.terms) {
55✔
469
                if (extractedTableAlias === "")
118✔
470
                    extractedTableAlias = Sql.getTableAlias(tableName, term);
118✔
471
            }
118✔
472
        }
55✔
473

643✔
474
        return extractedTableAlias;
643✔
475
    }
643✔
476

1✔
477
    /**
1✔
478
     * Create table definition array from select string.
1✔
479
     * @param {String} statement - full sql select statement.
1✔
480
     * @returns {String[][]} - table definition array.
1✔
481
     */
1✔
482
    static getReferencedTableNames(statement) {
1✔
483
        const ast = SqlParse.sql2ast(statement);
11✔
484
        return this.getReferencedTableNamesFromAst(ast);
11✔
485
    }
11✔
486

1✔
487
    /**
1✔
488
     * Create table definition array from select AST.
1✔
489
     * @param {Object} ast - AST for SELECT. 
1✔
490
     * @returns {any[]} - table definition array.
1✔
491
     * * [0] - table name.
1✔
492
     * * [1] - sheet tab name
1✔
493
     * * [2] - cache seconds
1✔
494
     * * [3] - output column title flag
1✔
495
     */
1✔
496
    static getReferencedTableNamesFromAst(ast) {
1✔
497
        const DEFAULT_CACHE_SECONDS = 60;
31✔
498
        const DEFAULT_COLUMNS_OUTPUT = true;
31✔
499
        const tableSet = new Map();
31✔
500

31✔
501
        Sql.extractAstTables(ast, tableSet);
31✔
502

31✔
503
        const tableList = [];
31✔
504
        // @ts-ignore
31✔
505
        for (const key of tableSet.keys()) {
31✔
506
            const tableDef = [key, key, DEFAULT_CACHE_SECONDS, DEFAULT_COLUMNS_OUTPUT];
45✔
507

45✔
508
            tableList.push(tableDef);
45✔
509
        }
45✔
510

31✔
511
        return tableList;
31✔
512
    }
31✔
513

1✔
514
    /**
1✔
515
     * Search for all referenced tables in SELECT.
1✔
516
     * @param {Object} ast - AST for SELECT.
1✔
517
     * @param {Map<String,String>} tableSet  - Function updates this map of table names and alias name.
1✔
518
     */
1✔
519
    static extractAstTables(ast, tableSet) {
1✔
520
        Sql.getTableNamesFrom(ast, tableSet);
79✔
521
        Sql.getTableNamesJoin(ast, tableSet);
79✔
522
        Sql.getTableNamesUnion(ast, tableSet);
79✔
523
        Sql.getTableNamesWhereIn(ast, tableSet);
79✔
524
        Sql.getTableNamesWhereTerms(ast, tableSet);
79✔
525
        Sql.getTableNamesCorrelatedSelect(ast, tableSet);
79✔
526
    }
79✔
527

1✔
528
    /**
1✔
529
     * Search for referenced table in FROM or JOIN part of select.
1✔
530
     * @param {Object} ast - AST for SELECT.
1✔
531
     * @param {Map<String,String>} tableSet  - Function updates this map of table names and alias name.
1✔
532
     */
1✔
533
    static getTableNamesFrom(ast, tableSet) {
1✔
534
        let fromAst = ast.FROM;
79✔
535
        while (typeof fromAst !== 'undefined') {
79✔
536
            if (typeof fromAst.isDerived === 'undefined') {
74✔
537
                tableSet.set(fromAst.table.toUpperCase(), typeof fromAst.as === 'undefined' ? '' : fromAst.as.toUpperCase());
70!
538
            }
70✔
539
            fromAst = fromAst.FROM;
74✔
540
        }
74✔
541
    }
79✔
542

1✔
543
    /**
1✔
544
    * Search for referenced table in FROM or JOIN part of select.
1✔
545
    * @param {Object} ast - AST for SELECT.
1✔
546
    * @param {Map<String,String>} tableSet  - Function updates this map of table names and alias name.
1✔
547
    */
1✔
548
    static getTableNamesJoin(ast, tableSet) {
1✔
549

79✔
550
        if (typeof ast.JOIN === 'undefined')
79✔
551
            return;
79✔
552

3✔
553
        for (const astItem of ast.JOIN) {
79✔
554
            tableSet.set(astItem.table.toUpperCase(), typeof astItem.as === 'undefined' ? '' : astItem.as.toUpperCase());
6!
555
        }
6✔
556
    }
79✔
557

1✔
558
    /**
1✔
559
     * Check if input is iterable.
1✔
560
     * @param {any} input - Check this object to see if it can be iterated. 
1✔
561
     * @returns {Boolean} - true - can be iterated.  false - cannot be iterated.
1✔
562
     */
1✔
563
    static isIterable(input) {
1✔
564
        if (input === null || input === undefined) {
593!
565
            return false
×
566
        }
×
567

593✔
568
        return typeof input[Symbol.iterator] === 'function'
593✔
569
    }
593✔
570

1✔
571
    /**
1✔
572
     * Searches for table names within SELECT (union, intersect, except) statements.
1✔
573
     * @param {Object} ast - AST for SELECT
1✔
574
     * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
1✔
575
     */
1✔
576
    static getTableNamesUnion(ast, tableSet) {
1✔
577
        const astRecursiveTableBlocks = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
79✔
578

79✔
579
        for (const block of astRecursiveTableBlocks) {
79✔
580
            if (typeof ast[block] !== 'undefined') {
316✔
581
                for (const unionAst of ast[block]) {
1✔
582
                    this.extractAstTables(unionAst, tableSet);
1✔
583
                }
1✔
584
            }
1✔
585
        }
316✔
586
    }
79✔
587

1✔
588
    /**
1✔
589
     * Searches for tables names within SELECT (in, exists) statements.
1✔
590
     * @param {Object} ast - AST for SELECT
1✔
591
     * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
1✔
592
     */
1✔
593
    static getTableNamesWhereIn(ast, tableSet) {
1✔
594
        //  where IN ().
79✔
595
        const subQueryTerms = ["IN", "NOT IN", "EXISTS", "NOT EXISTS"]
79✔
596
        if (typeof ast.WHERE !== 'undefined' && (subQueryTerms.indexOf(ast.WHERE.operator) !== -1)) {
79✔
597
            this.extractAstTables(ast.WHERE.right, tableSet);
4✔
598
        }
4✔
599

79✔
600
        if (subQueryTerms.indexOf(ast.operator) !== -1) {
79✔
601
            this.extractAstTables(ast.right, tableSet);
3✔
602
        }
3✔
603
    }
79✔
604

1✔
605
    /**
1✔
606
     * Search WHERE to find referenced table names.
1✔
607
     * @param {Object} ast -  AST to search.
1✔
608
     * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
1✔
609
     */
1✔
610
    static getTableNamesWhereTerms(ast, tableSet) {
1✔
611
        if (typeof ast.WHERE !== 'undefined' && typeof ast.WHERE.terms !== 'undefined') {
79✔
612
            for (const term of ast.WHERE.terms) {
4✔
613
                this.extractAstTables(term, tableSet);
9✔
614
            }
9✔
615
        }
4✔
616
    }
79✔
617

1✔
618
    /**
1✔
619
     * Search for table references in the WHERE condition.
1✔
620
     * @param {Object} ast -  AST to search.
1✔
621
     * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name. 
1✔
622
     */
1✔
623
    static getTableNamesWhereCondition(ast, tableSet) {
1✔
624
        const lParts = typeof ast.left === 'string' ? ast.left.split(".") : [];
21✔
625
        if (lParts.length > 1) {
21✔
626
            tableSet.set(lParts[0].toUpperCase(), "");
10✔
627
        }
10✔
628
        const rParts = typeof ast.right === 'string' ? ast.right.split(".") : [];
21✔
629
        if (rParts.length > 1) {
21✔
630
            tableSet.set(rParts[0].toUpperCase(), "");
5✔
631
        }
5✔
632
        if (typeof ast.terms !== 'undefined') {
21✔
633
            for (const term of ast.terms) {
1✔
634
                Sql.getTableNamesWhereCondition(term, tableSet);
2✔
635
            }
2✔
636
        }
1✔
637
    }
21✔
638

1✔
639
    /**
1✔
640
     * Search CORRELATES sub-query for table names.
1✔
641
     * @param {*} ast - AST to search
1✔
642
     * @param {*} tableSet - Function updates this map of table names and alias name.
1✔
643
     */
1✔
644
    static getTableNamesCorrelatedSelect(ast, tableSet) {
1✔
645
        if (typeof ast.SELECT !== 'undefined') {
79✔
646
            for (const term of ast.SELECT) {
70✔
647
                if (typeof term.subQuery !== 'undefined' && term.subQuery !== null) {
94✔
648
                    this.extractAstTables(term.subQuery, tableSet);
1✔
649
                }
1✔
650
            }
94✔
651
        }
70✔
652
    }
79✔
653

1✔
654
    /**
1✔
655
     * Search a property of AST for table alias name.
1✔
656
     * @param {String} tableName - Table name to find in AST.
1✔
657
     * @param {Object} ast - AST of SELECT.
1✔
658
     * @param {String} astBlock - AST property to search.
1✔
659
     * @returns {String} - Alias name or "" if not found.
1✔
660
     */
1✔
661
    static locateAstTableAlias(tableName, ast, astBlock) {
1✔
662
        if (typeof ast[astBlock] === 'undefined')
1,255✔
663
            return "";
1,255✔
664

593✔
665
        let block = [ast[astBlock]];
593✔
666
        if (this.isIterable(ast[astBlock])) {
1,255✔
667
            block = ast[astBlock];
67✔
668
        }
67✔
669

593✔
670
        for (const astItem of block) {
1,255✔
671
            if (tableName === astItem.table.toUpperCase() && astItem.as !== "") {
665✔
672
                return astItem.as;
39✔
673
            }
39✔
674
        }
665✔
675

553✔
676
        return "";
553✔
677
    }
1,255✔
678

1✔
679
    /**
1✔
680
     * Load SELECT data and return in double array.
1✔
681
     * @param {Object} selectAst - Abstract Syntax Tree of SELECT
1✔
682
     * @returns {any[][]} - double array useable by Google Sheet in custom function return value.
1✔
683
     * * First row of data will be column name if column title output was requested.
1✔
684
     * * First Array Index - ROW
1✔
685
     * * Second Array Index - COLUMN
1✔
686
     */
1✔
687
    select(selectAst) {
1✔
688
        let recordIDs = [];
306✔
689
        let viewTableData = [];
306✔
690
        let ast = selectAst;
306✔
691

306✔
692
        if (typeof ast.FROM === 'undefined')
306✔
693
            throw new Error("Missing keyword FROM");
306✔
694

305✔
695
        //  Manipulate AST to add GROUP BY if DISTINCT keyword.
305✔
696
        ast = Sql.distinctField(ast);
305✔
697

305✔
698
        //  Manipulate AST add pivot fields.
305✔
699
        ast = this.pivotField(ast);
305✔
700

305✔
701
        const view = new SelectTables(ast, this.tables, this.bindData);
305✔
702

305✔
703
        //  JOIN tables to create a derived table.
305✔
704
        view.join(ast);                 // skipcq: JS-D008
305✔
705

305✔
706
        //  Get the record ID's of all records matching WHERE condition.
305✔
707
        recordIDs = view.whereCondition(ast);
305✔
708

305✔
709
        //  Get selected data records.
305✔
710
        viewTableData = view.getViewData(recordIDs);
305✔
711

305✔
712
        //  Compress the data.
305✔
713
        viewTableData = view.groupBy(ast, viewTableData);
305✔
714

305✔
715
        //  Sort our selected data.
305✔
716
        view.orderBy(ast, viewTableData);
305✔
717

305✔
718
        //  Remove fields referenced but not included in SELECT field list.
305✔
719
        view.removeTempColumns(viewTableData);
305✔
720

305✔
721
        if (typeof ast.LIMIT !== 'undefined') {
306✔
722
            const maxItems = ast.LIMIT.nb;
2✔
723
            if (viewTableData.length > maxItems)
2✔
724
                viewTableData.splice(maxItems);
2✔
725
        }
2✔
726

287✔
727
        //  Apply SET rules for various union types.
287✔
728
        viewTableData = this.unionSets(ast, viewTableData);
287✔
729

287✔
730
        if (this.columnTitle) {
306✔
731
            viewTableData.unshift(view.getColumnTitles(this.columnTableNameReplacement));
127✔
732
        }
127✔
733

286✔
734
        if (viewTableData.length === 0) {
306✔
735
            viewTableData.push([""]);
7✔
736
        }
7✔
737

286✔
738
        if (viewTableData.length === 1 && viewTableData[0].length === 0) {
306✔
739
            viewTableData[0] = [""];
8✔
740
        }
8✔
741

286✔
742
        return viewTableData;
286✔
743
    }
306✔
744

1✔
745
    /**
1✔
746
     * If 'GROUP BY' is not set and 'DISTINCT' column is specified, update AST to add 'GROUP BY'.
1✔
747
     * @param {Object} ast - Abstract Syntax Tree for SELECT.
1✔
748
     * @returns {Object} - Updated AST to include GROUP BY when DISTINCT field used.
1✔
749
     */
1✔
750
    static distinctField(ast) {
1✔
751
        const astFields = ast.SELECT;
305✔
752

305✔
753
        if (astFields.length > 0) {
305✔
754
            const firstField = astFields[0].name.toUpperCase();
305✔
755
            if (firstField.startsWith("DISTINCT")) {
305✔
756
                astFields[0].name = firstField.replace("DISTINCT", "").trim();
7✔
757

7✔
758
                if (typeof ast['GROUP BY'] === 'undefined') {
7✔
759
                    const groupBy = [];
7✔
760

7✔
761
                    for (const astItem of astFields) {
7✔
762
                        groupBy.push({ column: astItem.name });
7✔
763
                    }
7✔
764

7✔
765
                    ast["GROUP BY"] = groupBy;
7✔
766
                }
7✔
767
            }
7✔
768
        }
305✔
769

305✔
770
        return ast;
305✔
771
    }
305✔
772

1✔
773
    /**
1✔
774
     * Add new column to AST for every AGGREGATE function and unique pivot column data.
1✔
775
     * @param {Object} ast - AST which is checked to see if a PIVOT is used.
1✔
776
     * @returns {Object} - Updated AST containing SELECT FIELDS for the pivot data OR original AST if no pivot.
1✔
777
     */
1✔
778
    pivotField(ast) {
1✔
779
        //  If we are doing a PIVOT, it then requires a GROUP BY.
305✔
780
        if (typeof ast.PIVOT !== 'undefined') {
305✔
781
            if (typeof ast['GROUP BY'] === 'undefined')
7✔
782
                throw new Error("PIVOT requires GROUP BY");
7✔
783
        }
7✔
784
        else
298✔
785
            return ast;
298✔
786

6✔
787
        // These are all of the unique PIVOT field data points.
6✔
788
        const pivotFieldData = this.getUniquePivotData(ast);
6✔
789

6✔
790
        ast.SELECT = Sql.addCalculatedPivotFieldsToAst(ast, pivotFieldData);
6✔
791

6✔
792
        return ast;
6✔
793
    }
305✔
794

1✔
795
    /**
1✔
796
     * Find distinct pivot column data.
1✔
797
     * @param {Object} ast - Abstract Syntax Tree containing the PIVOT option.
1✔
798
     * @returns {any[][]} - All unique data points found in the PIVOT field for the given SELECT.
1✔
799
     */
1✔
800
    getUniquePivotData(ast) {
1✔
801
        const pivotAST = {};
6✔
802

6✔
803
        pivotAST.SELECT = ast.PIVOT;
6✔
804
        pivotAST.SELECT[0].name = `DISTINCT ${pivotAST.SELECT[0].name}`;
6✔
805
        pivotAST.FROM = ast.FROM;
6✔
806
        pivotAST.WHERE = ast.WHERE;
6✔
807

6✔
808
        const pivotSql = new Sql()
6✔
809
            .enableColumnTitle(false)
6✔
810
            .setBindValues(this.bindData)
6✔
811
            .copyTableData(this.getTables());
6✔
812

6✔
813
        // These are all of the unique PIVOT field data points.
6✔
814
        const tableData = pivotSql.execute(pivotAST);
6✔
815

6✔
816
        return tableData;
6✔
817
    }
6✔
818

1✔
819
    /**
1✔
820
     * Add new calculated fields to the existing SELECT fields.  A field is add for each combination of
1✔
821
     * aggregate function and unqiue pivot data points.  The CASE function is used for each new field.
1✔
822
     * A test is made if the column data equal the pivot data.  If it is, the aggregate function data 
1✔
823
     * is returned, otherwise null.  The GROUP BY is later applied and the appropiate pivot data will
1✔
824
     * be calculated.
1✔
825
     * @param {Object} ast - AST to be updated.
1✔
826
     * @param {any[][]} pivotFieldData - Table data with unique pivot field data points. 
1✔
827
     * @returns {Object} - Abstract Sytax Tree with new SELECT fields with a CASE for each pivot data and aggregate function.
1✔
828
     */
1✔
829
    static addCalculatedPivotFieldsToAst(ast, pivotFieldData) {
1✔
830
        const newPivotAstFields = [];
6✔
831

6✔
832
        for (const selectField of ast.SELECT) {
6✔
833
            //  If this is an aggregrate function, we will add one for every pivotFieldData item
17✔
834
            const functionNameRegex = /^\w+\s*(?=\()/;
17✔
835
            const matches = selectField.name.match(functionNameRegex)
17✔
836
            if (matches !== null && matches.length > 0) {
17✔
837
                const args = SelectTables.parseForFunctions(selectField.name, matches[0].trim());
11✔
838

11✔
839
                for (const fld of pivotFieldData) {
11✔
840
                    const caseTxt = `${matches[0]}(CASE WHEN ${ast.PIVOT[0].name} = '${fld}' THEN ${args[1]} ELSE 'null' END)`;
44✔
841
                    const asField = `${fld[0]} ${typeof selectField.as !== 'undefined' && selectField.as !== "" ? selectField.as : selectField.name}`;
44✔
842
                    newPivotAstFields.push({ name: caseTxt, as: asField });
44✔
843
                }
44✔
844
            }
11✔
845
            else
6✔
846
                newPivotAstFields.push(selectField);
6✔
847
        }
17✔
848

6✔
849
        return newPivotAstFields;
6✔
850
    }
6✔
851

1✔
852
    /**
1✔
853
     * If any SET commands are found (like UNION, INTERSECT,...) the additional SELECT is done.  The new
1✔
854
     * data applies the SET rule against the income viewTableData, and the result data set is returned.
1✔
855
     * @param {Object} ast - SELECT AST.
1✔
856
     * @param {any[][]} viewTableData - SELECTED data before UNION.
1✔
857
     * @returns {any[][]} - New data with set rules applied.
1✔
858
     */
1✔
859
    unionSets(ast, viewTableData) {
1✔
860
        const unionTypes = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
287✔
861
        let unionTableData = viewTableData;
287✔
862

287✔
863
        for (const type of unionTypes) {
287✔
864
            if (typeof ast[type] !== 'undefined') {
1,145✔
865
                const unionSQL = new Sql()
9✔
866
                    .setBindValues(this.bindData)
9✔
867
                    .copyTableData(this.getTables());
9✔
868
                for (const union of ast[type]) {
9✔
869
                    const unionData = unionSQL.execute(union);
11✔
870
                    if (unionTableData.length > 0 && unionData.length > 0 && unionTableData[0].length !== unionData[0].length)
11✔
871
                        throw new Error(`Invalid ${type}.  Selected field counts do not match.`);
11✔
872

10✔
873
                    switch (type) {
10✔
874
                        case "UNION":
11✔
875
                            //  Remove duplicates.
4✔
876
                            unionTableData = Sql.appendUniqueRows(unionTableData, unionData);
4✔
877
                            break;
4✔
878

11✔
879
                        case "UNION ALL":
11✔
880
                            //  Allow duplicates.
4✔
881
                            unionTableData = unionTableData.concat(unionData);
4✔
882
                            break;
4✔
883

11✔
884
                        case "INTERSECT":
11✔
885
                            //  Must exist in BOTH tables.
1✔
886
                            unionTableData = Sql.intersectRows(unionTableData, unionData);
1✔
887
                            break;
1✔
888

11✔
889
                        case "EXCEPT":
11✔
890
                            //  Remove from first table all rows that match in second table.
1✔
891
                            unionTableData = Sql.exceptRows(unionTableData, unionData);
1✔
892
                            break;
1✔
893

11✔
894
                        default:
11!
895
                            throw new Error(`Internal error.  Unsupported UNION type: ${type}`);
×
896
                    }
11✔
897
                }
11✔
898
            }
8✔
899
        }
1,145✔
900

286✔
901
        return unionTableData;
286✔
902
    }
287✔
903

1✔
904
    /**
1✔
905
     * Appends any row in newData that does not exist in srcData.
1✔
906
     * @param {any[][]} srcData - existing table data
1✔
907
     * @param {any[][]} newData - new table data
1✔
908
     * @returns {any[][]} - srcData rows PLUS any row in newData that is NOT in srcData.
1✔
909
     */
1✔
910
    static appendUniqueRows(srcData, newData) {
1✔
911
        const srcMap = new Map();
4✔
912

4✔
913
        for (const srcRow of srcData) {
4✔
914
            srcMap.set(srcRow.join("::"), true);
13✔
915
        }
13✔
916

4✔
917
        for (const newRow of newData) {
4✔
918
            const key = newRow.join("::");
22✔
919
            if (!srcMap.has(key)) {
22✔
920
                srcData.push(newRow);
20✔
921
                srcMap.set(key, true);
20✔
922
            }
20✔
923
        }
22✔
924
        return srcData;
4✔
925
    }
4✔
926

1✔
927
    /**
1✔
928
     * Finds the rows that are common between srcData and newData
1✔
929
     * @param {any[][]} srcData - table data
1✔
930
     * @param {any[][]} newData - table data
1✔
931
     * @returns {any[][]} - returns only rows that intersect srcData and newData.
1✔
932
     */
1✔
933
    static intersectRows(srcData, newData) {
1✔
934
        const srcMap = new Map();
1✔
935
        const intersectTable = [];
1✔
936

1✔
937
        for (const srcRow of srcData) {
1✔
938
            srcMap.set(srcRow.join("::"), true);
10✔
939
        }
10✔
940

1✔
941
        for (const newRow of newData) {
1✔
942
            if (srcMap.has(newRow.join("::"))) {
5✔
943
                intersectTable.push(newRow);
1✔
944
            }
1✔
945
        }
5✔
946
        return intersectTable;
1✔
947
    }
1✔
948

1✔
949
    /**
1✔
950
     * Returns all rows in srcData MINUS any rows that match it from newData.
1✔
951
     * @param {any[][]} srcData - starting table
1✔
952
     * @param {any[][]} newData  - minus table (if it matches srcData row)
1✔
953
     * @returns {any[][]} - srcData MINUS newData
1✔
954
     */
1✔
955
    static exceptRows(srcData, newData) {
1✔
956
        const srcMap = new Map();
1✔
957
        let rowNum = 0;
1✔
958
        for (const srcRow of srcData) {
1✔
959
            srcMap.set(srcRow.join("::"), rowNum);
5✔
960
            rowNum++;
5✔
961
        }
5✔
962

1✔
963
        const removeRowNum = [];
1✔
964
        for (const newRow of newData) {
1✔
965
            const key = newRow.join("::");
2✔
966
            if (srcMap.has(key)) {
2✔
967
                removeRowNum.push(srcMap.get(key));
2✔
968
            }
2✔
969
        }
2✔
970

1✔
971
        removeRowNum.sort(function (a, b) { return b - a });
1✔
972
        for (rowNum of removeRowNum) {
1✔
973
            srcData.splice(rowNum, 1);
2✔
974
        }
2✔
975

1✔
976
        return srcData;
1✔
977
    }
1✔
978
}
1✔
979

1✔
980
/**
1✔
981
 * Store and retrieve bind data for use in WHERE portion of SELECT statement.
1✔
982
 */
1✔
983
class BindData {
1✔
984
    constructor() {
1✔
985
        this.clear();
421✔
986
    }
421✔
987

1✔
988
    /**
1✔
989
     * Reset the bind data.
1✔
990
     */
1✔
991
    clear() {
1✔
992
        this.next = 1;
422✔
993
        this.bindMap = new Map();
422✔
994
        this.bindQueue = [];
422✔
995
    }
422✔
996

1✔
997
    /**
1✔
998
     * Add bind data 
1✔
999
     * @param {any} data - bind data
1✔
1000
     * @returns {String} - bind variable name for reference in SQL.  e.g.  first data point would return '?1'.
1✔
1001
     */
1✔
1002
    add(data) {
1✔
1003
        const key = `?${this.next.toString()}`;
169✔
1004
        this.bindMap.set(key, data);
169✔
1005
        this.bindQueue.push(data);
169✔
1006

169✔
1007
        this.next++;
169✔
1008

169✔
1009
        return key;
169✔
1010
    }
169✔
1011

1✔
1012
    /**
1✔
1013
     * Add a list of bind data points.
1✔
1014
     * @param {any[]} bindList 
1✔
1015
     */
1✔
1016
    addList(bindList) {
1✔
1017
        for (const data of bindList) {
114✔
1018
            this.add(data);
3✔
1019
        }
3✔
1020
    }
114✔
1021

1✔
1022
    /**
1✔
1023
     * Pull out a bind data entry.
1✔
1024
     * @param {String} name - Get by name or get NEXT if empty.
1✔
1025
     * @returns {any}
1✔
1026
     */
1✔
1027
    get(name = "") {
1✔
1028
        return name === '' ? this.bindQueue.shift() : this.bindMap.get(name);
166!
1029
    }
166✔
1030

1✔
1031
    /**
1✔
1032
     * Return the ordered list of bind data.
1✔
1033
     * @returns {any[]} - Current list of bind data.
1✔
1034
     */
1✔
1035
    getBindDataList() {
1✔
1036
        return this.bindQueue;
259✔
1037
    }
259✔
1038
}
1✔
1039

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

© 2025 Coveralls, Inc