• 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.28
/src/SimpleParser.js
1
//  Remove comments for testing in NODE
1✔
2
/*  *** DEBUG START ***
1✔
3
export { SqlParse };
1✔
4
//  *** DEBUG END  ***/
1✔
5

1✔
6
//  Code inspired from:  https://github.com/dsferruzza/simpleSqlParser
1✔
7

1✔
8
/** Parse SQL SELECT statement and convert into Abstract Syntax Tree */
1✔
9
class SqlParse {
1✔
10
    /**
1✔
11
     * 
1✔
12
     * @param {String} cond 
1✔
13
     * @returns {String}
1✔
14
     */
1✔
15
    static sqlCondition2JsCondition(cond) {
1✔
16
        const ast = SqlParse.sql2ast(`SELECT A FROM c WHERE ${cond}`);
56✔
17
        let sqlData = "";
56✔
18

56✔
19
        if (typeof ast.WHERE !== 'undefined') {
56✔
20
            const conditions = ast.WHERE;
56✔
21
            if (typeof conditions.logic === 'undefined')
56✔
22
                sqlData = SqlParse.resolveSqlCondition("OR", [conditions]);
56✔
23
            else
3✔
24
                sqlData = SqlParse.resolveSqlCondition(conditions.logic, conditions.terms);
3✔
25

56✔
26
        }
56✔
27

56✔
28
        return sqlData;
56✔
29
    }
56✔
30

1✔
31
    /**
1✔
32
     * Parse a query
1✔
33
     * @param {String} query 
1✔
34
     * @returns {Object}
1✔
35
     */
1✔
36
    static sql2ast(query) {
1✔
37
        // Define which words can act as separator
276✔
38
        const myKeyWords = SqlParse.generateUsedKeywordList(query);
276✔
39
        const [parts_name, parts_name_escaped] = SqlParse.generateSqlSeparatorWords(myKeyWords);
276✔
40

276✔
41
        //  Include brackets around separate selects used in things like UNION, INTERSECT...
276✔
42
        let modifiedQuery = SqlParse.sqlStatementSplitter(query);
276✔
43

276✔
44
        // Hide words defined as separator but written inside brackets in the query
276✔
45
        modifiedQuery = SqlParse.hideInnerSql(modifiedQuery, parts_name_escaped, SqlParse.protect);
276✔
46

276✔
47
        // Write the position(s) in query of these separators
276✔
48
        const parts_order = SqlParse.getPositionsOfSqlParts(modifiedQuery, parts_name);
276✔
49

276✔
50
        // Delete duplicates (caused, for example, by JOIN and INNER JOIN)
276✔
51
        SqlParse.removeDuplicateEntries(parts_order);
276✔
52

276✔
53
        // Generate protected word list to reverse the use of protect()
276✔
54
        let words = parts_name_escaped.slice(0);
276✔
55
        words = words.map(function (item) {
276✔
56
            return SqlParse.protect(item);
1,782✔
57
        });
276✔
58

276✔
59
        // Split parts
276✔
60
        const parts = modifiedQuery.split(new RegExp(parts_name_escaped.join('|'), 'i'));
276✔
61

276✔
62
        // Unhide words precedently hidden with protect()
276✔
63
        for (let i = 0; i < parts.length; i++) {
276✔
64
            parts[i] = SqlParse.hideInnerSql(parts[i], words, SqlParse.unprotect);
1,115✔
65
        }
1,115✔
66

276✔
67
        // Analyze parts
276✔
68
        const result = SqlParse.analyzeParts(parts_order, parts);
276✔
69

276✔
70
        if (typeof result.FROM !== 'undefined' && typeof result.FROM.FROM !== 'undefined' && typeof result.FROM.FROM.as !== 'undefined' && result.FROM.FROM.as !== '') {
276✔
71
            //   Subquery FROM creates an ALIAS name, which is then used as FROM table name.
13✔
72
            result.FROM.table = result.FROM.FROM.as;
13✔
73
            result.FROM.isDerived = true;
13✔
74
        }
13✔
75

275✔
76
        return result;
275✔
77
    }
276✔
78

1✔
79
    /**
1✔
80
    * 
1✔
81
    * @param {String} logic 
1✔
82
    * @param {Object} terms 
1✔
83
    * @returns {String}
1✔
84
    */
1✔
85
    static resolveSqlCondition(logic, terms) {
1✔
86
        let jsCondition = "";
56✔
87

56✔
88
        for (const cond of terms) {
56✔
89
            if (typeof cond.logic === 'undefined') {
59✔
90
                if (jsCondition !== "" && logic === "AND") {
59✔
91
                    jsCondition += " && ";
2✔
92
                }
2✔
93
                else if (jsCondition !== "" && logic === "OR") {
57✔
94
                    jsCondition += " || ";
1✔
95
                }
1✔
96

59✔
97
                jsCondition += ` ${cond.left}`;
59✔
98
                if (cond.operator === "=")
59✔
99
                    jsCondition += " == ";
59✔
100
                else
13✔
101
                    jsCondition += ` ${cond.operator}`;
13✔
102
                jsCondition += ` ${cond.right}`;
59✔
103
            }
59✔
104
            else {
×
105
                jsCondition += SqlParse.resolveSqlCondition(cond.logic, cond.terms);
×
106
            }
×
107
        }
59✔
108

56✔
109
        return jsCondition;
56✔
110
    }
56✔
111

1✔
112
    /**
1✔
113
     * 
1✔
114
     * @param {String} query
1✔
115
     * @returns {String[]} 
1✔
116
     */
1✔
117
    static generateUsedKeywordList(query) {
1✔
118
        const generatedList = new Set();
276✔
119
        // Define which words can act as separator
276✔
120
        const keywords = ['SELECT', 'FROM', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'FULL JOIN', 'ORDER BY', 'GROUP BY', 'HAVING', 'WHERE', 'LIMIT', 'UNION ALL', 'UNION', 'INTERSECT', 'EXCEPT', 'PIVOT'];
276✔
121

276✔
122
        const modifiedQuery = query.toUpperCase();
276✔
123

276✔
124
        for (const word of keywords) {
276✔
125
            let pos = 0;
4,692✔
126
            while (pos !== -1) {
4,692✔
127
                pos = modifiedQuery.indexOf(word, pos);
5,787✔
128

5,787✔
129
                if (pos !== -1) {
5,787✔
130
                    generatedList.add(query.substring(pos, pos + word.length));
1,095✔
131
                    pos++;
1,095✔
132
                }
1,095✔
133
            }
5,787✔
134
        }
4,692✔
135

276✔
136
        // @ts-ignore
276✔
137
        return [...generatedList];
276✔
138
    }
276✔
139

1✔
140
    /**
1✔
141
     * 
1✔
142
     * @param {String[]} keywords 
1✔
143
     * @returns {String[][]}
1✔
144
     */
1✔
145
    static generateSqlSeparatorWords(keywords) {
1✔
146
        let parts_name = keywords.map(function (item) {
276✔
147
            return `${item} `;
891✔
148
        });
276✔
149
        parts_name = parts_name.concat(keywords.map(function (item) {
276✔
150
            return `${item}(`;
891✔
151
        }));
276✔
152
        const parts_name_escaped = parts_name.map(function (item) {
276✔
153
            return item.replace('(', '[\\(]');
1,782✔
154
        });
276✔
155

276✔
156
        return [parts_name, parts_name_escaped];
276✔
157
    }
276✔
158

1✔
159
    /**
1✔
160
     * 
1✔
161
     * @param {String} src 
1✔
162
     * @returns {String}
1✔
163
     */
1✔
164
    static sqlStatementSplitter(src) {
1✔
165
        let newStr = src;
276✔
166

276✔
167
        // Define which words can act as separator
276✔
168
        const reg = SqlParse.makeSqlPartsSplitterRegEx(["UNION ALL", "UNION", "INTERSECT", "EXCEPT"]);
276✔
169

276✔
170
        const matchedUnions = newStr.match(reg);
276✔
171
        if (matchedUnions === null || matchedUnions.length === 0)
276✔
172
            return newStr;
276✔
173

10✔
174
        let prefix = "";
10✔
175
        const parts = [];
10✔
176
        let pos = newStr.search(matchedUnions[0]);
10✔
177
        if (pos > 0) {
10✔
178
            prefix = newStr.substring(0, pos);
10✔
179
            newStr = newStr.substring(pos + matchedUnions[0].length);
10✔
180
        }
10✔
181

10✔
182
        for (let i = 1; i < matchedUnions.length; i++) {
276✔
183
            const match = matchedUnions[i];
2✔
184
            pos = newStr.search(match);
2✔
185

2✔
186
            parts.push(newStr.substring(0, pos));
2✔
187
            newStr = newStr.substring(pos + match.length);
2✔
188
        }
2✔
189
        if (newStr.length > 0)
10✔
190
            parts.push(newStr);
10✔
191

10✔
192
        newStr = prefix;
10✔
193
        for (let i = 0; i < matchedUnions.length; i++) {
276✔
194
            newStr += `${matchedUnions[i]} (${parts[i]}) `;
12✔
195
        }
12✔
196

10✔
197
        return newStr;
10✔
198
    }
276✔
199

1✔
200
    /**
1✔
201
     * 
1✔
202
     * @param {String[]} keywords 
1✔
203
     * @returns {RegExp}
1✔
204
     */
1✔
205
    static makeSqlPartsSplitterRegEx(keywords) {
1✔
206
        // Define which words can act as separator
276✔
207
        let parts_name = keywords.map(function (item) {
276✔
208
            return `${item} `;
1,104✔
209
        });
276✔
210
        parts_name = parts_name.concat(keywords.map(function (item) {
276✔
211
            return `${item}(`;
1,104✔
212
        }));
276✔
213
        parts_name = parts_name.concat(parts_name.map(function (item) {
276✔
214
            return item.toLowerCase();
2,208✔
215
        }));
276✔
216
        const parts_name_escaped = parts_name.map(function (item) {
276✔
217
            return item.replace('(', '[\\(]');
4,416✔
218
        });
276✔
219

276✔
220
        return new RegExp(parts_name_escaped.join('|'), 'gi');
276✔
221
    }
276✔
222

1✔
223
    /**
1✔
224
     * 
1✔
225
     * @param {String} str 
1✔
226
     * @param {String[]} parts_name_escaped
1✔
227
     * @param {Object} replaceFunction
1✔
228
     */
1✔
229
    static hideInnerSql(str, parts_name_escaped, replaceFunction) {
1✔
230
        if (str.indexOf("(") === -1 && str.indexOf(")") === -1)
1,391✔
231
            return str;
1,391✔
232

268✔
233
        let bracketCount = 0;
268✔
234
        let endCount = -1;
268✔
235
        let newStr = str;
268✔
236

268✔
237
        for (let i = newStr.length - 1; i >= 0; i--) {
1,391✔
238
            const ch = newStr.charAt(i);
27,673✔
239

27,673✔
240
            if (ch === ")") {
27,673✔
241
                bracketCount++;
514✔
242

514✔
243
                if (bracketCount === 1) {
514✔
244
                    endCount = i;
400✔
245
                }
400✔
246
            }
514✔
247
            else if (ch === "(") {
27,159✔
248
                bracketCount--;
514✔
249
                if (bracketCount === 0) {
514✔
250

400✔
251
                    let query = newStr.substring(i, endCount + 1);
400✔
252

400✔
253
                    // Hide words defined as separator but written inside brackets in the query
400✔
254
                    query = query.replace(new RegExp(parts_name_escaped.join('|'), 'gi'), replaceFunction);
400✔
255

400✔
256
                    newStr = newStr.substring(0, i) + query + newStr.substring(endCount + 1);
400✔
257
                }
400✔
258
            }
514✔
259
        }
27,673✔
260
        return newStr;
268✔
261
    }
1,391✔
262

1✔
263
    /**
1✔
264
     * 
1✔
265
     * @param {String} modifiedQuery 
1✔
266
     * @param {String[]} parts_name 
1✔
267
     * @returns {String[]}
1✔
268
     */
1✔
269
    static getPositionsOfSqlParts(modifiedQuery, parts_name) {
1✔
270
        // Write the position(s) in query of these separators
276✔
271
        const parts_order = [];
276✔
272
        function realNameCallback(_match, name) {
276✔
273
            return name;
891✔
274
        }
891✔
275
        parts_name.forEach(function (item) {
276✔
276
            let pos = 0;
1,782✔
277
            let part = 0;
1,782✔
278

1,782✔
279
            do {
1,782✔
280
                part = modifiedQuery.indexOf(item, pos);
2,673✔
281
                if (part !== -1) {
2,673✔
282
                    const realName = item.replace(/^((\w|\s)+?)\s?\(?$/i, realNameCallback);
891✔
283

891✔
284
                    if (typeof parts_order[part] === 'undefined' || parts_order[part].length < realName.length) {
891✔
285
                        parts_order[part] = realName;        // Position won't be exact because the use of protect()  (above) and unprotect() alter the query string ; but we just need the order :)
887✔
286
                    }
887✔
287

891✔
288
                    pos = part + realName.length;
891✔
289
                }
891✔
290
            }
2,673✔
291
            while (part !== -1);
1,782✔
292
        });
276✔
293

276✔
294
        return parts_order;
276✔
295
    }
276✔
296

1✔
297
    /**
1✔
298
     * Delete duplicates (caused, for example, by JOIN and INNER JOIN)
1✔
299
     * @param {String[]} parts_order
1✔
300
     */
1✔
301
    static removeDuplicateEntries(parts_order) {
1✔
302
        let busy_until = 0;
276✔
303
        parts_order.forEach(function (item, key) {
276✔
304
            if (busy_until > key)
887✔
305
                delete parts_order[key];
887✔
306
            else {
839✔
307
                busy_until = key + item.length;
839✔
308

839✔
309
                // Replace JOIN by INNER JOIN
839✔
310
                if (item.toUpperCase() === 'JOIN')
839✔
311
                    parts_order[key] = 'INNER JOIN';
839✔
312
            }
839✔
313
        });
276✔
314
    }
276✔
315

1✔
316
    /**
1✔
317
     * Add some # inside a string to avoid it to match a regex/split
1✔
318
     * @param {String} str 
1✔
319
     * @returns {String}
1✔
320
     */
1✔
321
    static protect(str) {
1✔
322
        let result = '#';
1,986✔
323
        const length = str.length;
1,986✔
324
        for (let i = 0; i < length; i++) {
1,986✔
325
            result += `${str[i]}#`;
15,128✔
326
        }
15,128✔
327
        return result;
1,986✔
328
    }
1,986✔
329

1✔
330
    /**
1✔
331
     * Restore a string output by protect() to its original state
1✔
332
     * @param {String} str 
1✔
333
     * @returns {String}
1✔
334
     */
1✔
335
    static unprotect(str) {
1✔
336
        let result = '';
204✔
337
        const length = str.length;
204✔
338
        for (let i = 1; i < length; i = i + 2) result += str[i];
204✔
339
        return result;
204✔
340
    }
204✔
341

1✔
342
    /**
1✔
343
     * 
1✔
344
     * @param {String[]} parts_order 
1✔
345
     * @param {String[]} parts 
1✔
346
     * @returns {Object}
1✔
347
     */
1✔
348
    static analyzeParts(parts_order, parts) {
1✔
349
        const result = {};
276✔
350
        let j = 0;
276✔
351
        parts_order.forEach(function (item, _key) {
276✔
352
            const itemName = item.toUpperCase();
839✔
353
            j++;
839✔
354
            const part_result = SelectKeywordAnalysis.analyze(item, parts[j]);
839✔
355

839✔
356
            if (typeof result[itemName] !== 'undefined') {
839✔
357
                if (typeof result[itemName] === 'string' || typeof result[itemName][0] === 'undefined') {
22✔
358
                    const tmp = result[itemName];
11✔
359
                    result[itemName] = [];
11✔
360
                    result[itemName].push(tmp);
11✔
361
                }
11✔
362

22✔
363
                result[itemName].push(part_result);
22✔
364
            }
22✔
365
            else {
816✔
366
                result[itemName] = part_result;
816✔
367
            }
816✔
368

839✔
369
        });
276✔
370

276✔
371
        // Reorganize joins
276✔
372
        SqlParse.reorganizeJoins(result);
276✔
373

276✔
374
        if (typeof result.JOIN !== 'undefined') {
276✔
375
            result.JOIN.forEach(function (item, key) {
30✔
376
                result.JOIN[key].cond = CondParser.parse(item.cond);
50✔
377
            });
30✔
378
        }
30✔
379

275✔
380
        SqlParse.reorganizeUnions(result);
275✔
381

275✔
382
        return result;
275✔
383
    }
276✔
384

1✔
385
    /**
1✔
386
     * 
1✔
387
     * @param {Object} result 
1✔
388
     */
1✔
389
    static reorganizeJoins(result) {
1✔
390
        const joinArr = [
275✔
391
            ['FULL JOIN', 'full'],
275✔
392
            ['RIGHT JOIN', 'right'],
275✔
393
            ['INNER JOIN', 'inner'],
275✔
394
            ['LEFT JOIN', 'left']
275✔
395
        ];
275✔
396

275✔
397
        for (const join of joinArr) {
275✔
398
            const [joinName, joinType] = join;
1,100✔
399
            SqlParse.reorganizeSpecificJoin(result, joinName, joinType);
1,100✔
400
        }
1,100✔
401
    }
275✔
402

1✔
403
    /**
1✔
404
     * 
1✔
405
     * @param {Object} result 
1✔
406
     * @param {String} joinName 
1✔
407
     * @param {String} joinType 
1✔
408
     */
1✔
409
    static reorganizeSpecificJoin(result, joinName, joinType) {
1✔
410
        if (typeof result[joinName] !== 'undefined') {
1,100✔
411
            if (typeof result.JOIN === 'undefined') result.JOIN = [];
30✔
412
            if (typeof result[joinName][0] !== 'undefined') {
30✔
413
                result[joinName].forEach(function (item) {
9✔
414
                    item.type = joinType;
29✔
415
                    result.JOIN.push(item);
29✔
416
                });
9✔
417
            }
9✔
418
            else {
21✔
419
                result[joinName].type = joinType;
21✔
420
                result.JOIN.push(result[joinName]);
21✔
421
            }
21✔
422
            delete result[joinName];
30✔
423
        }
30✔
424
    }
1,100✔
425

1✔
426
    /**
1✔
427
     * 
1✔
428
     * @param {Object} result 
1✔
429
     */
1✔
430
    static reorganizeUnions(result) {
1✔
431
        const astRecursiveTableBlocks = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
275✔
432

275✔
433
        for (const union of astRecursiveTableBlocks) {
275✔
434
            if (typeof result[union] === 'string') {
1,100✔
435
                result[union] = [SqlParse.sql2ast(SqlParse.parseUnion(result[union]))];
8✔
436
            }
8✔
437
            else if (typeof result[union] !== 'undefined') {
1,092✔
438
                for (let i = 0; i < result[union].length; i++) {
2✔
439
                    result[union][i] = SqlParse.sql2ast(SqlParse.parseUnion(result[union][i]));
4✔
440
                }
4✔
441
            }
2✔
442
        }
1,100✔
443
    }
275✔
444

1✔
445
    static parseUnion(inStr) {
1✔
446
        let unionString = inStr;
12✔
447
        if (unionString.startsWith("(") && unionString.endsWith(")")) {
12✔
448
            unionString = unionString.substring(1, unionString.length - 1);
12✔
449
        }
12✔
450

12✔
451
        return unionString;
12✔
452
    }
12✔
453
}
1✔
454

1✔
455
/*
1✔
456
 * LEXER & PARSER FOR SQL CONDITIONS
1✔
457
 * Inspired by https://github.com/DmitrySoshnikov/Essentials-of-interpretation
1✔
458
 */
1✔
459

1✔
460
/** Lexical analyzer for SELECT statement. */
1✔
461
class CondLexer {
1✔
462
    constructor(source) {
1✔
463
        this.source = source;
219✔
464
        this.cursor = 0;
219✔
465
        this.currentChar = "";
219✔
466
        this.startQuote = "";
219✔
467
        this.bracketCount = 0;
219✔
468

219✔
469
        this.readNextChar();
219✔
470
    }
219✔
471

1✔
472
    // Read the next character (or return an empty string if cursor is at the end of the source)
1✔
473
    readNextChar() {
1✔
474
        if (typeof this.source !== 'string') {
7,533✔
475
            this.currentChar = "";
1✔
476
        }
1✔
477
        else {
7,532✔
478
            this.currentChar = this.source[this.cursor++] || "";
7,532✔
479
        }
7,532✔
480
    }
7,533✔
481

1✔
482
    // Determine the next token
1✔
483
    readNextToken() {
1✔
484
        if (/\w/.test(this.currentChar))
2,366✔
485
            return this.readWord();
2,366✔
486
        if (/["'`]/.test(this.currentChar))
1,694✔
487
            return this.readString();
2,366✔
488
        if (/[()]/.test(this.currentChar))
1,616✔
489
            return this.readGroupSymbol();
2,366✔
490
        if (/[!=<>]/.test(this.currentChar))
1,538✔
491
            return this.readOperator();
2,366✔
492
        if (/[+\-*/%]/.test(this.currentChar))
1,292✔
493
            return this.readMathOperator();
2,366✔
494
        if (this.currentChar === '?')
1,280✔
495
            return this.readBindVariable();
2,366✔
496

1,217✔
497
        if (this.currentChar === "") {
2,366✔
498
            return { type: 'eot', value: '' };
218✔
499
        }
218✔
500

999✔
501
        this.readNextChar();
999✔
502
        return { type: 'empty', value: '' };
999✔
503
    }
2,366✔
504

1✔
505
    readWord() {
1✔
506
        let tokenValue = "";
672✔
507
        this.bracketCount = 0;
672✔
508
        let insideQuotedString = false;
672✔
509
        this.startQuote = "";
672✔
510

672✔
511
        while (/./.test(this.currentChar)) {
672✔
512
            // Check if we are in a string
5,976✔
513
            insideQuotedString = this.isStartOrEndOfString(insideQuotedString);
5,976✔
514

5,976✔
515
            if (this.isFinishedWord(insideQuotedString))
5,976✔
516
                break;
5,976✔
517

5,385✔
518
            tokenValue += this.currentChar;
5,385✔
519
            this.readNextChar();
5,385✔
520
        }
5,385✔
521

672✔
522
        if (/^(AND|OR)$/i.test(tokenValue)) {
672✔
523
            return { type: 'logic', value: tokenValue.toUpperCase() };
45✔
524
        }
45✔
525

627✔
526
        if (/^(IN|IS|NOT|LIKE|NOT EXISTS|EXISTS)$/i.test(tokenValue)) {
672✔
527
            return { type: 'operator', value: tokenValue.toUpperCase() };
47✔
528
        }
47✔
529

580✔
530
        return { type: 'word', value: tokenValue };
580✔
531
    }
672✔
532

1✔
533
    /**
1✔
534
     * 
1✔
535
     * @param {Boolean} insideQuotedString 
1✔
536
     * @returns {Boolean}
1✔
537
     */
1✔
538
    isStartOrEndOfString(insideQuotedString) {
1✔
539
        if (!insideQuotedString && /['"`]/.test(this.currentChar)) {
5,976✔
540
            this.startQuote = this.currentChar;
4✔
541

4✔
542
            return true;
4✔
543
        }
4✔
544
        else if (insideQuotedString && this.currentChar === this.startQuote) {
5,972✔
545
            //  End of quoted string.
4✔
546
            return false;
4✔
547
        }
4✔
548

5,968✔
549
        return insideQuotedString;
5,968✔
550
    }
5,976✔
551

1✔
552
    /**
1✔
553
     * 
1✔
554
     * @param {Boolean} insideQuotedString 
1✔
555
     * @returns {Boolean}
1✔
556
     */
1✔
557
    isFinishedWord(insideQuotedString) {
1✔
558
        if (insideQuotedString)
5,976✔
559
            return false;
5,976✔
560

5,968✔
561
        // Token is finished if there is a closing bracket outside a string and with no opening
5,968✔
562
        if (this.currentChar === ')' && this.bracketCount <= 0) {
5,976✔
563
            return true;
22✔
564
        }
22✔
565

5,946✔
566
        if (this.currentChar === '(') {
5,976✔
567
            this.bracketCount++;
21✔
568
        }
21✔
569
        else if (this.currentChar === ')') {
5,925✔
570
            this.bracketCount--;
21✔
571
        }
21✔
572

5,946✔
573
        // Token is finished if there is a operator symbol outside a string
5,946✔
574
        if (/[!=<>]/.test(this.currentChar)) {
5,976✔
575
            return true;
3✔
576
        }
3✔
577

5,943✔
578
        // Token is finished on the first space which is outside a string or a function
5,943✔
579
        return this.currentChar === ' ' && this.bracketCount <= 0;
5,976✔
580
    }
5,976✔
581

1✔
582
    readString() {
1✔
583
        let tokenValue = "";
78✔
584
        const quote = this.currentChar;
78✔
585

78✔
586
        tokenValue += this.currentChar;
78✔
587
        this.readNextChar();
78✔
588

78✔
589
        while (this.currentChar !== quote && this.currentChar !== "") {
78✔
590
            tokenValue += this.currentChar;
279✔
591
            this.readNextChar();
279✔
592
        }
279✔
593

78✔
594
        tokenValue += this.currentChar;
78✔
595
        this.readNextChar();
78✔
596

78✔
597
        // Handle this case : `table`.`column`
78✔
598
        if (this.currentChar === '.') {
78!
599
            tokenValue += this.currentChar;
×
600
            this.readNextChar();
×
601
            tokenValue += this.readString().value;
×
602

×
603
            return { type: 'word', value: tokenValue };
×
604
        }
×
605

78✔
606
        return { type: 'string', value: tokenValue };
78✔
607
    }
78✔
608

1✔
609
    readGroupSymbol() {
1✔
610
        const tokenValue = this.currentChar;
78✔
611
        this.readNextChar();
78✔
612

78✔
613
        return { type: 'group', value: tokenValue };
78✔
614
    }
78✔
615

1✔
616
    readOperator() {
1✔
617
        let tokenValue = this.currentChar;
246✔
618
        this.readNextChar();
246✔
619

246✔
620
        if (/[=<>]/.test(this.currentChar)) {
246✔
621
            tokenValue += this.currentChar;
40✔
622
            this.readNextChar();
40✔
623
        }
40✔
624

246✔
625
        return { type: 'operator', value: tokenValue };
246✔
626
    }
246✔
627

1✔
628
    readMathOperator() {
1✔
629
        const tokenValue = this.currentChar;
12✔
630
        this.readNextChar();
12✔
631

12✔
632
        return { type: 'mathoperator', value: tokenValue };
12✔
633
    }
12✔
634

1✔
635
    readBindVariable() {
1✔
636
        let tokenValue = this.currentChar;
63✔
637
        this.readNextChar();
63✔
638

63✔
639
        while (/\d/.test(this.currentChar)) {
63✔
640
            tokenValue += this.currentChar;
56✔
641
            this.readNextChar();
56✔
642
        }
56✔
643

63✔
644
        return { type: 'bindVariable', value: tokenValue };
63✔
645
    }
63✔
646
}
1✔
647

1✔
648
/** SQL Condition parser class. */
1✔
649
class CondParser {
1✔
650
    constructor(source) {
1✔
651
        this.lexer = new CondLexer(source);
219✔
652
        this.currentToken = {};
219✔
653

219✔
654
        this.readNextToken();
219✔
655
    }
219✔
656

1✔
657
    // Parse a string
1✔
658
    static parse(source) {
1✔
659
        return new CondParser(source).parseExpressionsRecursively();
219✔
660
    }
219✔
661

1✔
662
    // Read the next token (skip empty tokens)
1✔
663
    readNextToken() {
1✔
664
        this.currentToken = this.lexer.readNextToken();
1,367✔
665
        while (this.currentToken.type === 'empty')
1,367✔
666
            this.currentToken = this.lexer.readNextToken();
1,367✔
667
        return this.currentToken;
1,367✔
668
    }
1,367✔
669

1✔
670
    // Wrapper function ; parse the source
1✔
671
    parseExpressionsRecursively() {
1✔
672
        return this.parseLogicalExpression();
251✔
673
    }
251✔
674

1✔
675
    // Parse logical expressions (AND/OR)
1✔
676
    parseLogicalExpression() {
1✔
677
        let leftNode = this.parseConditionExpression();
251✔
678

251✔
679
        while (this.currentToken.type === 'logic') {
251✔
680
            const logic = this.currentToken.value;
44✔
681
            this.readNextToken();
44✔
682

44✔
683
            const rightNode = this.parseConditionExpression();
44✔
684

44✔
685
            // If we are chaining the same logical operator, add nodes to existing object instead of creating another one
44✔
686
            if (typeof leftNode.logic !== 'undefined' && leftNode.logic === logic && typeof leftNode.terms !== 'undefined')
44✔
687
                leftNode.terms.push(rightNode);
44✔
688
            else {
39✔
689
                const terms = [leftNode, rightNode];
39✔
690
                leftNode = { 'logic': logic, 'terms': terms.slice(0) };
39✔
691
            }
39✔
692
        }
44✔
693

251✔
694
        return leftNode;
251✔
695
    }
251✔
696

1✔
697
    // Parse conditions ([word/string] [operator] [word/string])
1✔
698
    parseConditionExpression() {
1✔
699
        let left = this.parseBaseExpression();
295✔
700

295✔
701
        if (this.currentToken.type !== 'operator') {
295✔
702
            return left;
33✔
703
        }
33✔
704

262✔
705
        let operator = this.currentToken.value;
262✔
706
        this.readNextToken();
262✔
707

262✔
708
        // If there are 2 adjacent operators, join them with a space (exemple: IS NOT)
262✔
709
        if (this.currentToken.type === 'operator') {
295✔
710
            operator += ` ${this.currentToken.value}`;
7✔
711
            this.readNextToken();
7✔
712
        }
7✔
713

262✔
714
        let right = null;
262✔
715
        if (this.currentToken.type === 'group' && (operator === 'EXISTS' || operator === 'NOT EXISTS')) {
295✔
716
            [left, right] = this.parseSelectExistsSubQuery();
3✔
717
        } else {
295✔
718
            right = this.parseBaseExpression(operator);
259✔
719
        }
259✔
720

262✔
721
        return { operator, left, right };
262✔
722
    }
295✔
723

1✔
724
    /**
1✔
725
     * 
1✔
726
     * @returns {Object[]}
1✔
727
     */
1✔
728
    parseSelectExistsSubQuery() {
1✔
729
        let rightNode = null;
3✔
730
        const leftNode = '""';
3✔
731

3✔
732
        this.readNextToken();
3✔
733
        if (this.currentToken.type === 'word' && this.currentToken.value === 'SELECT') {
3✔
734
            rightNode = this.parseSelectIn("", true);
3✔
735
            if (this.currentToken.type === 'group') {
3✔
736
                this.readNextToken();
3✔
737
            }
3✔
738
        }
3✔
739

3✔
740
        return [leftNode, rightNode];
3✔
741
    }
3✔
742

1✔
743
    // Parse base items
1✔
744
    /**
1✔
745
     * 
1✔
746
     * @param {String} operator 
1✔
747
     * @returns {Object}
1✔
748
     */
1✔
749
    parseBaseExpression(operator = "") {
1✔
750
        let astNode = {};
554✔
751

554✔
752
        // If this is a word/string, return its value
554✔
753
        if (this.currentToken.type === 'word' || this.currentToken.type === 'string') {
554✔
754
            astNode = this.parseWordExpression();
461✔
755
        }
461✔
756
        // If this is a group, skip brackets and parse the inside
93✔
757
        else if (this.currentToken.type === 'group') {
93✔
758
            astNode = this.parseGroupExpression(operator);
32✔
759
        }
32✔
760
        else if (this.currentToken.type === 'bindVariable') {
61✔
761
            astNode = this.currentToken.value;
57✔
762
            this.readNextToken();
57✔
763
        }
57✔
764

554✔
765
        return astNode;
554✔
766
    }
554✔
767

1✔
768
    /**
1✔
769
     * 
1✔
770
     * @returns {Object}
1✔
771
     */
1✔
772
    parseWordExpression() {
1✔
773
        let astNode = this.currentToken.value;
461✔
774
        this.readNextToken();
461✔
775

461✔
776
        if (this.currentToken.type === 'mathoperator') {
461✔
777
            astNode += ` ${this.currentToken.value}`;
9✔
778
            this.readNextToken();
9✔
779
            while ((this.currentToken.type === 'mathoperator' || this.currentToken.type === 'word') && this.currentToken.type !== 'eot') {
9✔
780
                astNode += ` ${this.currentToken.value}`;
9✔
781
                this.readNextToken();
9✔
782
            }
9✔
783
        }
9✔
784

461✔
785
        return astNode;
461✔
786
    }
461✔
787

1✔
788
    /**
1✔
789
     * 
1✔
790
     * @param {String} operator 
1✔
791
     * @returns {Object}
1✔
792
     */
1✔
793
    parseGroupExpression(operator) {
1✔
794
        this.readNextToken();
32✔
795
        let astNode = this.parseExpressionsRecursively();
32✔
796

32✔
797
        const isSelectStatement = typeof astNode === "string" && astNode.toUpperCase() === 'SELECT';
32✔
798

32✔
799
        if (operator === 'IN' || isSelectStatement) {
32✔
800
            astNode = this.parseSelectIn(astNode, isSelectStatement);
31✔
801
        }
31✔
802
        else {
1✔
803
            //  Are we within brackets of mathmatical expression ?
1✔
804
            let inCurrentToken = this.currentToken;
1✔
805

1✔
806
            while (inCurrentToken.type !== 'group' && inCurrentToken.type !== 'eot') {
1!
807
                this.readNextToken();
×
808
                if (inCurrentToken.type !== 'group') {
×
809
                    astNode += ` ${inCurrentToken.value}`;
×
810
                }
×
811

×
812
                inCurrentToken = this.currentToken;
×
813
            }
×
814

1✔
815
        }
1✔
816

32✔
817
        this.readNextToken();
32✔
818

32✔
819
        return astNode;
32✔
820
    }
32✔
821

1✔
822
    /**
1✔
823
     * 
1✔
824
     * @param {Object} startAstNode 
1✔
825
     * @param {Boolean} isSelectStatement 
1✔
826
     * @returns {Object}
1✔
827
     */
1✔
828
    parseSelectIn(startAstNode, isSelectStatement) {
1✔
829
        let astNode = startAstNode;
34✔
830
        let inCurrentToken = this.currentToken;
34✔
831
        let bracketCount = 1;
34✔
832
        while (bracketCount !== 0 && inCurrentToken.type !== 'eot') {
34✔
833
            this.readNextToken();
229✔
834
            if (isSelectStatement) {
229✔
835
                astNode += ` ${inCurrentToken.value}`;
225✔
836
            }
225✔
837
            else {
4✔
838
                astNode += `, ${inCurrentToken.value}`;
4✔
839
            }
4✔
840

229✔
841
            inCurrentToken = this.currentToken;
229✔
842
            bracketCount += CondParser.groupBracketIncrementer(inCurrentToken);
229✔
843
        }
229✔
844

34✔
845
        if (isSelectStatement) {
34✔
846
            astNode = SqlParse.sql2ast(astNode);
32✔
847
        }
32✔
848

34✔
849
        return astNode;
34✔
850
    }
34✔
851

1✔
852
    static groupBracketIncrementer(inCurrentToken) {
1✔
853
        let diff = 0;
229✔
854
        if (inCurrentToken.type === 'group') {
229✔
855
            if (inCurrentToken.value === '(') {
42✔
856
                diff = 1;
4✔
857
            }
4✔
858
            else if (inCurrentToken.value === ')') {
38✔
859
                diff = -1;
38✔
860
            }
38✔
861
        }
42✔
862

229✔
863
        return diff
229✔
864
    }
229✔
865
}
1✔
866

1✔
867
/** Analyze each distinct component of SELECT statement. */
1✔
868
class SelectKeywordAnalysis {
1✔
869
    static analyze(itemName, part) {
1✔
870
        const keyWord = itemName.toUpperCase().replace(/ /g, '_');
839✔
871

839✔
872
        if (typeof SelectKeywordAnalysis[keyWord] === 'undefined') {
839!
873
            throw new Error(`Can't analyze statement ${itemName}`);
×
874
        }
×
875

839✔
876
        return SelectKeywordAnalysis[keyWord](part);
839✔
877
    }
839✔
878

1✔
879
    static SELECT(str) {
1✔
880
        const selectParts = SelectKeywordAnalysis.protect_split(',', str);
275✔
881
        const selectResult = selectParts.filter(function (item) {
275✔
882
            return item !== '';
565✔
883
        }).map(function (item) {
275✔
884
            //  Is there a column alias?
565✔
885
            const [name, as] = SelectKeywordAnalysis.getNameAndAlias(item);
565✔
886

565✔
887
            const splitPattern = /[\s()*/%+-]+/g;
565✔
888
            let terms = name.split(splitPattern);
565✔
889

565✔
890
            if (terms !== null) {
565✔
891
                const aggFunc = ["SUM", "MIN", "MAX", "COUNT", "AVG", "DISTINCT"];
565✔
892
                terms = (aggFunc.indexOf(terms[0].toUpperCase()) === -1) ? terms : null;
565✔
893
            }
565✔
894
            if (name !== "*" && terms !== null && terms.length > 1) {
565✔
895
                const subQuery = SelectKeywordAnalysis.parseForCorrelatedSubQuery(item);
73✔
896
                return { name, terms, as, subQuery };
73✔
897
            }
73✔
898
            return { name, as };
492✔
899
        });
275✔
900

275✔
901
        return selectResult;
275✔
902
    }
275✔
903

1✔
904
    static FROM(str) {
1✔
905
        const subqueryAst = this.parseForCorrelatedSubQuery(str);
275✔
906
        if (subqueryAst !== null) {
275✔
907
            //  If there is a subquery creating a DERIVED table, it must have a derived table name.
14✔
908
            //  Extract this subquery AS tableName.
14✔
909
            const [, alias] = SelectKeywordAnalysis.getNameAndAlias(str);
14✔
910
            if (alias !== "" && typeof subqueryAst.FROM !== 'undefined') {
14✔
911
                subqueryAst.FROM.as = alias.toUpperCase();
13✔
912
            }
13✔
913

14✔
914
            return subqueryAst;
14✔
915
        }
14✔
916

261✔
917
        let fromResult = str.split(',');
261✔
918
        fromResult = fromResult.map(function (item) {
261✔
919
            return SelectKeywordAnalysis.trim(item);
261✔
920
        });
261✔
921
        fromResult = fromResult.map(function (item) {
261✔
922
            const [table, as] = SelectKeywordAnalysis.getNameAndAlias(item);
261✔
923
            return { table, as };
261✔
924
        });
261✔
925
        return fromResult[0];
261✔
926
    }
275✔
927

1✔
928
    static LEFT_JOIN(str) {
1✔
929
        return SelectKeywordAnalysis.allJoins(str);
30✔
930
    }
30✔
931

1✔
932
    static INNER_JOIN(str) {
1✔
933
        return SelectKeywordAnalysis.allJoins(str);
12✔
934
    }
12✔
935

1✔
936
    static RIGHT_JOIN(str) {
1✔
937
        return SelectKeywordAnalysis.allJoins(str);
3✔
938
    }
3✔
939

1✔
940
    static FULL_JOIN(str) {
1✔
941
        return SelectKeywordAnalysis.allJoins(str);
5✔
942
    }
5✔
943

1✔
944
    static allJoins(str) {
1✔
945
        const strParts = str.toUpperCase().split(' ON ');
50✔
946
        const table = strParts[0].split(' AS ');
50✔
947
        const joinResult = {};
50✔
948
        joinResult.table = SelectKeywordAnalysis.trim(table[0]);
50✔
949
        joinResult.as = SelectKeywordAnalysis.trim(table[1]) || '';
50✔
950
        joinResult.cond = SelectKeywordAnalysis.trim(strParts[1]);
50✔
951

50✔
952
        return joinResult;
50✔
953
    }
50✔
954

1✔
955
    static WHERE(str) {
1✔
956
        return CondParser.parse(str);
167✔
957
    }
167✔
958

1✔
959
    static ORDER_BY(str) {
1✔
960
        const strParts = str.split(',');
29✔
961
        const orderByResult = [];
29✔
962
        strParts.forEach(function (item, _key) {
29✔
963
            const order_by = /([\w.]+)\s*(ASC|DESC)?/gi;
30✔
964
            const orderData = order_by.exec(item);
30✔
965
            if (orderData !== null) {
30✔
966
                const tmp = {};
30✔
967
                tmp.column = SelectKeywordAnalysis.trim(orderData[1]);
30✔
968
                tmp.order = SelectKeywordAnalysis.trim(orderData[2]);
30✔
969
                if (typeof orderData[2] === 'undefined') {
30✔
970
                    const orderParts = item.trim().split(" ");
26✔
971
                    if (orderParts.length > 1)
26✔
972
                        throw new Error(`Invalid ORDER BY:  ${item}`);
26✔
973
                    tmp.order = "ASC";
25✔
974
                }
25✔
975
                orderByResult.push(tmp);
29✔
976
            }
29✔
977
        });
29✔
978
        return orderByResult;
29✔
979
    }
29✔
980

1✔
981
    static GROUP_BY(str) {
1✔
982
        const strParts = str.split(',');
20✔
983
        const groupByResult = [];
20✔
984
        strParts.forEach(function (item, _key) {
20✔
985
            const group_by = /([\w.]+)/gi;
23✔
986
            const groupData = group_by.exec(item);
23✔
987
            if (groupData !== null) {
23✔
988
                const tmp = {};
23✔
989
                tmp.column = SelectKeywordAnalysis.trim(groupData[1]);
23✔
990
                groupByResult.push(tmp);
23✔
991
            }
23✔
992
        });
20✔
993
        return groupByResult;
20✔
994
    }
20✔
995

1✔
996
    static PIVOT(str) {
1✔
997
        const strParts = str.split(',');
7✔
998
        const pivotResult = [];
7✔
999
        strParts.forEach(function (item, _key) {
7✔
1000
            const pivotOn = /([\w.]+)/gi;
7✔
1001
            const pivotData = pivotOn.exec(item);
7✔
1002
            if (pivotData !== null) {
7✔
1003
                const tmp = {};
7✔
1004
                tmp.name = SelectKeywordAnalysis.trim(pivotData[1]);
7✔
1005
                tmp.as = "";
7✔
1006
                pivotResult.push(tmp);
7✔
1007
            }
7✔
1008
        });
7✔
1009
        return pivotResult;
7✔
1010
    }
7✔
1011

1✔
1012
    static LIMIT(str) {
1✔
1013
        const limitResult = {};
2✔
1014
        limitResult.nb = parseInt(str, 10);
2✔
1015
        limitResult.from = 0;
2✔
1016
        return limitResult;
2✔
1017
    }
2✔
1018

1✔
1019
    static HAVING(str) {
1✔
1020
        return CondParser.parse(str);
2✔
1021
    }
2✔
1022

1✔
1023
    static UNION(str) {
1✔
1024
        return SelectKeywordAnalysis.trim(str);
6✔
1025
    }
6✔
1026

1✔
1027
    static UNION_ALL(str) {
1✔
1028
        return SelectKeywordAnalysis.trim(str);
4✔
1029
    }
4✔
1030

1✔
1031
    static INTERSECT(str) {
1✔
1032
        return SelectKeywordAnalysis.trim(str);
1✔
1033
    }
1✔
1034

1✔
1035
    static EXCEPT(str) {
1✔
1036
        return SelectKeywordAnalysis.trim(str);
1✔
1037
    }
1✔
1038

1✔
1039
    /**
1✔
1040
     * 
1✔
1041
     * @param {String} selectField 
1✔
1042
     * @returns {Object}
1✔
1043
     */
1✔
1044
    static parseForCorrelatedSubQuery(selectField) {
1✔
1045
        let subQueryAst = null;
348✔
1046

348✔
1047
        const regExp = /\(\s*(SELECT[\s\S]+)\)/;
348✔
1048
        const matches = regExp.exec(selectField.toUpperCase());
348✔
1049

348✔
1050
        if (matches !== null && matches.length > 1) {
348✔
1051
            subQueryAst = SqlParse.sql2ast(matches[1]);
19✔
1052
        }
19✔
1053

348✔
1054
        return subQueryAst;
348✔
1055
    }
348✔
1056

1✔
1057
    // Split a string using a separator, only if this separator isn't beetween brackets
1✔
1058
    /**
1✔
1059
     * 
1✔
1060
     * @param {String} separator 
1✔
1061
     * @param {String} str 
1✔
1062
     * @returns {String[]}
1✔
1063
     */
1✔
1064
    static protect_split(separator, str) {
1✔
1065
        const sep = '######';
275✔
1066

275✔
1067
        let inQuotedString = false;
275✔
1068
        let quoteChar = "";
275✔
1069
        let bracketCount = 0;
275✔
1070
        let newStr = "";
275✔
1071
        for (const c of str) {
275✔
1072
            if (!inQuotedString && /['"`]/.test(c)) {
8,395✔
1073
                inQuotedString = true;
65✔
1074
                quoteChar = c;
65✔
1075
            }
65✔
1076
            else if (inQuotedString && c === quoteChar) {
8,330✔
1077
                inQuotedString = false;
65✔
1078
            }
65✔
1079
            else if (!inQuotedString && c === '(') {
8,265✔
1080
                bracketCount++;
153✔
1081
            }
153✔
1082
            else if (!inQuotedString && c === ')') {
8,112✔
1083
                bracketCount--;
153✔
1084
            }
153✔
1085

8,395✔
1086
            if (c === separator && (bracketCount > 0 || inQuotedString)) {
8,395✔
1087
                newStr += sep;
64✔
1088
            }
64✔
1089
            else {
8,331✔
1090
                newStr += c;
8,331✔
1091
            }
8,331✔
1092
        }
8,395✔
1093

275✔
1094
        let strParts = newStr.split(separator);
275✔
1095
        strParts = strParts.map(function (item) {
275✔
1096
            return SelectKeywordAnalysis.trim(item.replace(new RegExp(sep, 'g'), separator));
565✔
1097
        });
275✔
1098

275✔
1099
        return strParts;
275✔
1100
    }
275✔
1101

1✔
1102
    static trim(str) {
1✔
1103
        if (typeof str === 'string')
1,078✔
1104
            return str.trim();
1,078✔
1105
        return str;
69✔
1106
    }
1,078✔
1107

1✔
1108
    /**
1✔
1109
    * If an ALIAS is specified after 'AS', return the field/table name and the alias.
1✔
1110
    * @param {String} item 
1✔
1111
    * @returns {String[]}
1✔
1112
    */
1✔
1113
    static getNameAndAlias(item) {
1✔
1114
        let realName = item;
840✔
1115
        let alias = "";
840✔
1116
        const lastAs = SelectKeywordAnalysis.lastIndexOfOutsideLiteral(item.toUpperCase(), " AS ");
840✔
1117
        if (lastAs !== -1) {
840✔
1118
            const subStr = item.substring(lastAs + 4).trim();
81✔
1119
            if (subStr.length > 0) {
81✔
1120
                alias = subStr;
81✔
1121
                //  Remove quotes, if any.
81✔
1122
                if ((subStr.startsWith("'") && subStr.endsWith("'")) ||
81✔
1123
                    (subStr.startsWith('"') && subStr.endsWith('"')) ||
81!
1124
                    (subStr.startsWith('[') && subStr.endsWith(']')))
81✔
1125
                    alias = subStr.substring(1, subStr.length - 1);
81✔
1126

81✔
1127
                //  Remove everything after 'AS'.
81✔
1128
                realName = item.substring(0, lastAs);
81✔
1129
            }
81✔
1130
        }
81✔
1131

840✔
1132
        return [realName, alias];
840✔
1133
    }
840✔
1134

1✔
1135
    /**
1✔
1136
     * 
1✔
1137
     * @param {String} srcString 
1✔
1138
     * @param {String} searchString 
1✔
1139
     * @returns {Number}
1✔
1140
     */
1✔
1141
    static lastIndexOfOutsideLiteral(srcString, searchString) {
1✔
1142
        let index = -1;
840✔
1143
        let inQuote = "";
840✔
1144

840✔
1145
        for (let i = 0; i < srcString.length; i++) {
840✔
1146
            const ch = srcString.charAt(i);
11,542✔
1147

11,542✔
1148
            if (inQuote !== "") {
11,542✔
1149
                //  The ending quote.
571✔
1150
                if ((inQuote === "'" && ch === "'") || (inQuote === '"' && ch === '"') || (inQuote === "[" && ch === "]"))
571!
1151
                    inQuote = "";
571✔
1152
            }
571✔
1153
            else if ("\"'[".indexOf(ch) !== -1) {
10,971✔
1154
                //  The starting quote.
79✔
1155
                inQuote = ch;
79✔
1156
            }
79✔
1157
            else if (srcString.substring(i).startsWith(searchString)) {
10,892✔
1158
                //  Matched search.
111✔
1159
                index = i;
111✔
1160
            }
111✔
1161
        }
11,542✔
1162

840✔
1163
        return index;
840✔
1164
    }
840✔
1165
}
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

© 2025 Coveralls, Inc