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

llnl / dftracer-utils / 29806785540

21 Jul 2026 06:21AM UTC coverage: 52.837% (+0.2%) from 52.66%
29806785540

Pull #99

github

web-flow
Merge 28252dedf into 06bc84ec9
Pull Request #99: Support CM time_metric (NS/MS/SEC/US) across reader and viz

41074 of 100799 branches covered (40.75%)

Branch coverage included in aggregate %.

1063 of 1183 new or added lines in 30 files covered. (89.86%)

32 existing lines in 7 files now uncovered.

36302 of 45643 relevant lines covered (79.53%)

23461.74 hits per line

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

69.68
/src/dftracer/utils/utilities/common/query/parser.cpp
1
#include <dftracer/utils/utilities/common/query/parser.h>
2
#include <dftracer/utils/utilities/common/query/pattern.h>
3

4
#include <algorithm>
5
#include <cctype>
6
#include <charconv>
7
#include <memory>
8
#include <regex>
9
#include <sstream>
10
#include <string>
11

12
namespace dftracer::utils::utilities::common::query {
13

14
std::string QueryError::format() const {
4✔
15
    std::ostringstream os;
4!
16
    os << "Query parse error at column " << column << ":\n";
4!
17
    os << "  " << source << '\n';
4!
18
    os << "  " << indicator << '\n';
4!
19
    os << "  " << message << '\n';
4!
20
    return os.str();
6!
21
}
4✔
22

23
QueryParseError::QueryParseError(QueryError err)
3✔
24
    : DFTUtilsException(ErrorCode::QUERY, err.format()), err_(std::move(err)) {}
3!
25

26
namespace {
27

28
bool is_ident_start(char c) {
3,149✔
29
    return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
3,149!
30
}
31

32
bool is_ident_char(char c) {
9,140✔
33
    return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '.';
9,140✔
34
}
35

36
bool iequals(std::string_view a, std::string_view b) {
19,647✔
37
    if (a.size() != b.size()) return false;
19,647✔
38
    return std::equal(a.begin(), a.end(), b.begin(),
5,315✔
39
                      [](unsigned char ca, unsigned char cb) {
6,685✔
40
                          return std::tolower(ca) == std::tolower(cb);
6,685✔
41
                      });
2,658✔
42
}
9,845✔
43

44
QueryError make_error(std::string_view source, std::size_t col, std::size_t len,
16✔
45
                      std::string msg) {
46
    std::string ind(col, ' ');
16!
47
    if (len > 0) {
16✔
48
        ind += '^';
16!
49
        for (std::size_t i = 1; i < len; ++i) ind += '~';
42✔
50
    } else {
8✔
51
        ind += '^';
×
52
    }
53
    return QueryError{std::move(msg), col, std::string(source), std::move(ind)};
24!
54
}
16✔
55

56
}  // namespace
57

58
dftracer::utils::expected<std::vector<Token>, QueryError> tokenize(
1,459✔
59
    std::string_view input) {
60
    std::vector<Token> tokens;
1,459✔
61
    std::size_t pos = 0;
1,460✔
62

63
    auto skip_ws = [&]() {
10,546✔
64
        while (pos < input.size() &&
28,358✔
65
               std::isspace(static_cast<unsigned char>(input[pos]))) {
14,660✔
66
            ++pos;
6,306✔
67
        }
68
    };
5,644✔
69

70
    while (true) {
4,915✔
71
        skip_ws();
9,819!
72
        if (pos >= input.size()) break;
9,820✔
73

74
        std::size_t start = pos;
8,365✔
75
        char c = input[pos];
8,365✔
76

77
        // Operators
78
        if (c == '=' && pos + 1 < input.size() && input[pos + 1] == '=') {
8,364!
79
            tokens.push_back({TokenKind::OP_EQ, input.substr(start, 2), start});
1,649!
80
            pos += 2;
1,649✔
81
            continue;
1,649✔
82
        }
83
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,717✔
84
            tokens.push_back({TokenKind::OP_NE, input.substr(start, 2), start});
8!
85
            pos += 2;
8✔
86
            continue;
8✔
87
        }
88
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '~') {
6,710!
89
            if (pos + 2 < input.size() && input[pos + 2] == '*') {
10!
NEW
90
                tokens.push_back(
×
NEW
91
                    {TokenKind::OP_NIREGEX, input.substr(start, 3), start});
×
NEW
92
                pos += 3;
×
93
            } else {
94
                tokens.push_back(
10!
95
                    {TokenKind::OP_NREGEX, input.substr(start, 2), start});
10!
96
                pos += 2;
10✔
97
            }
98
            continue;
10✔
99
        }
100
        if (c == '>' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,699!
101
            tokens.push_back({TokenKind::OP_GE, input.substr(start, 2), start});
167!
102
            pos += 2;
168✔
103
            continue;
168✔
104
        }
105
        if (c == '<' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,528!
106
            tokens.push_back({TokenKind::OP_LE, input.substr(start, 2), start});
143!
107
            pos += 2;
143✔
108
            continue;
143✔
109
        }
110
        if (c == '>') {
6,384✔
111
            tokens.push_back({TokenKind::OP_GT, input.substr(start, 1), start});
46!
112
            ++pos;
46✔
113
            continue;
46✔
114
        }
115
        if (c == '<') {
6,338✔
116
            tokens.push_back({TokenKind::OP_LT, input.substr(start, 1), start});
4!
117
            ++pos;
4✔
118
            continue;
4✔
119
        }
120
        if (c == '~') {
6,334✔
121
            if (pos + 1 < input.size() && input[pos + 1] == '*') {
26!
122
                tokens.push_back(
8!
123
                    {TokenKind::OP_IREGEX, input.substr(start, 2), start});
8!
124
                pos += 2;
8✔
125
            } else {
4✔
126
                tokens.push_back(
18!
127
                    {TokenKind::OP_REGEX, input.substr(start, 1), start});
18!
128
                ++pos;
18✔
129
            }
130
            continue;
26✔
131
        }
132

133
        // Punctuation
134
        if (c == '(') {
6,308✔
135
            tokens.push_back(
10!
136
                {TokenKind::LPAREN, input.substr(start, 1), start});
10!
137
            ++pos;
10✔
138
            continue;
10✔
139
        }
140
        if (c == ')') {
6,298✔
141
            tokens.push_back(
10!
142
                {TokenKind::RPAREN, input.substr(start, 1), start});
10!
143
            ++pos;
10✔
144
            continue;
10✔
145
        }
146
        if (c == '[') {
6,288✔
147
            tokens.push_back(
104!
148
                {TokenKind::LBRACKET, input.substr(start, 1), start});
104!
149
            ++pos;
104✔
150
            continue;
104✔
151
        }
152
        if (c == ']') {
6,184✔
153
            tokens.push_back(
104!
154
                {TokenKind::RBRACKET, input.substr(start, 1), start});
104!
155
            ++pos;
104✔
156
            continue;
104✔
157
        }
158
        if (c == ',') {
6,080✔
159
            tokens.push_back({TokenKind::COMMA, input.substr(start, 1), start});
374!
160
            ++pos;
374✔
161
            continue;
374✔
162
        }
163

164
        // Strings
165
        if (c == '"' || c == '\'') {
5,706✔
166
            char quote = c;
2,100✔
167
            ++pos;
2,100✔
168
            while (pos < input.size() && input[pos] != quote) {
13,513✔
169
                if (input[pos] == '\\' && pos + 1 < input.size()) {
11,408✔
170
                    pos += 2;
4✔
171
                } else {
2✔
172
                    ++pos;
11,409✔
173
                }
174
            }
175
            if (pos >= input.size()) {
2,112✔
176
                return dftracer::utils::unexpected(make_error(
3!
177
                    input, start, pos - start, "Unterminated string literal"));
2!
178
            }
179
            ++pos;  // consume closing quote
2,104✔
180
            tokens.push_back({TokenKind::STRING,
2,103!
181
                              input.substr(start + 1, pos - start - 2), start});
2,104!
182
            continue;
2,103✔
183
        }
1,051✔
184

185
        // Numbers (including negative)
186
        if (std::isdigit(static_cast<unsigned char>(c)) ||
3,607✔
187
            (c == '-' && pos + 1 < input.size() &&
1,581!
188
             std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
7✔
189
            bool is_float = false;
455✔
190
            ++pos;
455✔
191
            while (pos < input.size() &&
5,909✔
192
                   (std::isdigit(static_cast<unsigned char>(input[pos])) ||
2,825✔
193
                    input[pos] == '.' || input[pos] == 'e' ||
214✔
194
                    input[pos] == 'E' || input[pos] == '+' ||
206!
195
                    input[pos] == '-')) {
198!
196
                if (input[pos] == '.' || input[pos] == 'e' ||
3,911✔
197
                    input[pos] == 'E') {
2,605!
198
                    is_float = true;
8✔
199
                }
4✔
200
                ++pos;
2,623✔
201
            }
202
            auto text = input.substr(start, pos - start);
455!
203
            tokens.push_back(
456!
204
                {is_float ? TokenKind::FLOAT : TokenKind::INT, text, start});
456✔
205
            continue;
453✔
206
        }
225✔
207

208
        // Identifiers and keywords
209
        if (is_ident_start(c)) {
3,148!
210
            ++pos;
3,145✔
211
            while (pos < input.size() && is_ident_char(input[pos])) {
10,740✔
212
                ++pos;
6,019✔
213
            }
214
            auto text = input.substr(start, pos - start);
3,151!
215
            TokenKind kind = TokenKind::IDENT;
3,148✔
216
            if (iequals(text, "and"))
3,148!
217
                kind = TokenKind::KW_AND;
276✔
218
            else if (iequals(text, "or"))
2,872!
219
                kind = TokenKind::KW_OR;
471✔
220
            else if (iequals(text, "not"))
2,401!
221
                kind = TokenKind::KW_NOT;
36✔
222
            else if (iequals(text, "in"))
2,364!
223
                kind = TokenKind::KW_IN;
128✔
224
            else if (iequals(text, "like"))
2,238!
225
                kind = TokenKind::KW_LIKE;
14✔
226
            else if (iequals(text, "ilike"))
2,225!
227
                kind = TokenKind::KW_ILIKE;
10✔
228
            else if (iequals(text, "true"))
2,215!
229
                kind = TokenKind::KW_TRUE;
4✔
230
            else if (iequals(text, "false"))
2,212!
231
                kind = TokenKind::KW_FALSE;
4✔
232
            tokens.push_back({kind, text, start});
3,149!
233
            continue;
3,147✔
234
        }
1,571✔
235

236
        return dftracer::utils::unexpected(make_error(
3!
237
            input, start, 1, std::string("Unexpected character '") + c + "'"));
3!
238
    }
4,174✔
239

240
    tokens.push_back(
1,455!
241
        {TokenKind::END, input.substr(input.size(), 0), input.size()});
1,456!
242
    return tokens;
1,455!
243
}
1,458✔
244

245
// ============================================================
246
// Recursive Descent Parser
247
// ============================================================
248

249
namespace {
250

251
void append_escaped(std::string& out, char c) {
174✔
252
    switch (c) {
174!
253
        case '.':
254
        case '\\':
255
        case '+':
256
        case '*':
257
        case '?':
258
        case '(':
259
        case ')':
260
        case '[':
261
        case ']':
262
        case '{':
263
        case '}':
264
        case '^':
265
        case '$':
266
        case '|':
NEW
267
            out += '\\';
×
268
            [[fallthrough]];
269
        default:
87✔
270
            out += c;
174✔
271
    }
87✔
272
}
174✔
273

274
// Translate a SQL LIKE pattern to an anchored ECMAScript regex: '%' matches any
275
// run, '_' matches one char, everything else is literal.
276
std::string like_to_regex(const std::string& like) {
20✔
277
    std::string out = "^";
20!
278
    for (char c : like) {
124✔
279
        if (c == '%') {
104✔
280
            out += ".*";
32!
281
        } else if (c == '_') {
88✔
282
            out += '.';
4!
283
        } else {
2✔
284
            append_escaped(out, c);
68!
285
        }
286
    }
287
    out += '$';
20!
288
    return out;
20✔
289
}
10!
290

291
// Treat the whole literal as a substring to search for (unanchored).
292
std::string contains_to_regex(const std::string& sub) {
24✔
293
    std::string out;
24✔
294
    for (char c : sub) append_escaped(out, c);
130✔
295
    return out;
24✔
296
}
12!
297

298
dftracer::utils::expected<std::shared_ptr<CompiledPattern>, std::string>
299
compile_pattern(MatchOp op, const std::string& pattern) {
74✔
300
    std::string regex_src;
74✔
301
    auto flags = std::regex::ECMAScript;
74✔
302
    switch (op) {
74✔
303
        case MatchOp::LIKE:
6✔
304
            regex_src = like_to_regex(pattern);
12!
305
            break;
12✔
306
        case MatchOp::ILIKE:
4✔
307
            regex_src = like_to_regex(pattern);
8!
308
            flags |= std::regex::icase;
8!
309
            break;
8✔
310
        case MatchOp::REGEX:
12✔
311
            regex_src = pattern;
24!
312
            break;
24✔
313
        case MatchOp::IREGEX:
3✔
314
            regex_src = pattern;
6!
315
            flags |= std::regex::icase;
6!
316
            break;
6✔
317
        case MatchOp::ICONTAINS:
12✔
318
            regex_src = contains_to_regex(pattern);
24!
319
            flags |= std::regex::icase;
24!
320
            break;
24✔
321
    }
322
    try {
323
        auto cp = std::make_shared<CompiledPattern>();
74!
324
        cp->re = std::regex(regex_src, flags);
74✔
325
        return cp;
72!
326
    } catch (const std::regex_error& e) {
75!
327
        return dftracer::utils::unexpected(std::string(e.what()));
2!
328
    }
2!
329
}
75✔
330

331
class Parser {
332
   public:
333
    Parser(const std::vector<Token>& tokens, std::string_view source)
2,135✔
334
        : tokens_(tokens), source_(source) {}
2,847✔
335

336
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_query() {
1,423✔
337
        auto result = parse_or_expr();
1,423!
338
        if (!result) return result;
1,423✔
339
        if (current().kind != TokenKind::END) {
1,410!
340
            return dftracer::utils::unexpected(
×
341
                error("Expected 'and', 'or', or end of query, got '" +
×
342
                      std::string(current().text) + "'"));
×
343
        }
344
        return result;
1,411✔
345
    }
1,423✔
346

347
   private:
348
    const std::vector<Token>& tokens_;
349
    std::string_view source_;
350
    std::size_t pos_ = 0;
712✔
351

352
    const Token& current() const { return tokens_[pos_]; }
25,479✔
353

354
    const Token& advance() { return tokens_[pos_++]; }
7,544✔
355

356
    bool match(TokenKind kind) {
2,836✔
357
        if (current().kind == kind) {
2,836✔
358
            ++pos_;
596✔
359
            return true;
596✔
360
        }
361
        return false;
2,243✔
362
    }
1,421✔
363

364
    QueryError error(std::string msg) const {
10✔
365
        auto& tok = current();
10✔
366
        std::size_t len = tok.text.empty() ? 1 : tok.text.size();
10!
367
        return make_error(source_, tok.column, len, std::move(msg));
10!
368
    }
369

370
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_or_expr() {
1,433✔
371
        auto left = parse_and_expr();
1,433!
372
        if (!left) return left;
1,433✔
373
        while (current().kind == TokenKind::KW_OR) {
1,887✔
374
            advance();
466!
375
            auto right = parse_and_expr();
465!
376
            if (!right) return right;
466✔
377
            left = make_node(OrNode{std::move(*left), std::move(*right)});
466!
378
        }
466✔
379
        return left;
1,421✔
380
    }
1,431✔
381

382
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_and_expr() {
1,897✔
383
        auto left = parse_not_expr();
1,897!
384
        if (!left) return left;
1,897✔
385
        while (current().kind == TokenKind::KW_AND) {
2,141✔
386
            advance();
256!
387
            auto right = parse_not_expr();
255!
388
            if (!right) return right;
256✔
389
            left = make_node(AndNode{std::move(*left), std::move(*right)});
256!
390
        }
256✔
391
        return left;
1,885✔
392
    }
1,898✔
393

394
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_not_expr() {
2,162✔
395
        if (current().kind == TokenKind::KW_NOT) {
2,162✔
396
            advance();
10✔
397
            auto operand = parse_not_expr();
10!
398
            if (!operand) return operand;
10✔
399
            return make_node(NotNode{std::move(*operand)});
15!
400
        }
10✔
401
        return parse_primary();
2,152✔
402
    }
1,083✔
403

404
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_primary() {
2,151✔
405
        if (match(TokenKind::LPAREN)) {
2,151✔
406
            auto expr = parse_or_expr();
10!
407
            if (!expr) return expr;
10✔
408
            if (!match(TokenKind::RPAREN)) {
10!
409
                return dftracer::utils::unexpected(error("Expected ')'"));
×
410
            }
411
            return expr;
10✔
412
        }
10✔
413

414
        // Python-style substring: 'sub' in field / 'sub' not in field
415
        // (case-insensitive). Distinguished from "field in [array]" by the
416
        // string literal on the left.
417
        if (current().kind == TokenKind::STRING) {
2,143✔
418
            return parse_contains();
24!
419
        }
420

421
        if (current().kind != TokenKind::IDENT) {
2,121!
422
            return dftracer::utils::unexpected(
×
NEW
423
                error("Expected field name, string, or '(', got '" +
×
424
                      std::string(current().text) + "'"));
×
425
        }
426

427
        auto field = parse_field();
2,119!
428

429
        // Check for "in" or "not in"
430
        if (current().kind == TokenKind::KW_IN) {
2,119✔
431
            advance();
88!
432
            auto arr = parse_array();
88!
433
            if (!arr) return dftracer::utils::unexpected(arr.error());
88!
434
            return make_node(InNode{std::move(field), std::move(*arr)});
132!
435
        }
88✔
436
        if (current().kind == TokenKind::KW_NOT) {
2,030✔
437
            // Look ahead for "in", "like", or "ilike".
438
            if (pos_ + 1 < tokens_.size()) {
18✔
439
                auto next = tokens_[pos_ + 1].kind;
18✔
440
                if (next == TokenKind::KW_IN) {
18✔
441
                    advance();  // consume "not"
14!
442
                    advance();  // consume "in"
14!
443
                    auto arr = parse_array();
14!
444
                    if (!arr) return dftracer::utils::unexpected(arr.error());
14!
445
                    return make_node(
21!
446
                        NotInNode{std::move(field), std::move(*arr)});
28!
447
                }
14✔
448
                if (next == TokenKind::KW_LIKE || next == TokenKind::KW_ILIKE) {
4!
449
                    advance();  // consume "not"
4!
450
                    advance();  // consume "like"/"ilike"
4!
451
                    return parse_match(std::move(field),
6!
452
                                       next == TokenKind::KW_LIKE
2✔
453
                                           ? MatchOp::LIKE
2✔
454
                                           : MatchOp::ILIKE,
455
                                       /*negated=*/true);
2!
456
                }
457
            }
458
        }
459

460
        // Pattern match: like / ilike / ~ / ~* / !~ / !~*
461
        switch (current().kind) {
2,011!
462
            case TokenKind::KW_LIKE:
4✔
463
                advance();
8!
464
                return parse_match(std::move(field), MatchOp::LIKE, false);
8!
465
            case TokenKind::KW_ILIKE:
4✔
466
                advance();
8!
467
                return parse_match(std::move(field), MatchOp::ILIKE, false);
8!
468
            case TokenKind::OP_REGEX:
8✔
469
                advance();
16!
470
                return parse_match(std::move(field), MatchOp::REGEX, false);
16!
471
            case TokenKind::OP_IREGEX:
3✔
472
                advance();
6!
473
                return parse_match(std::move(field), MatchOp::IREGEX, false);
6!
474
            case TokenKind::OP_NREGEX:
4✔
475
                advance();
8!
476
                return parse_match(std::move(field), MatchOp::REGEX, true);
8!
477
            case TokenKind::OP_NIREGEX:
NEW
478
                advance();
×
NEW
479
                return parse_match(std::move(field), MatchOp::IREGEX, true);
×
480
            default:
983✔
481
                break;
1,968✔
482
        }
483

484
        // Comparison
485
        auto op = parse_comp_op();
1,968!
486
        if (!op) return dftracer::utils::unexpected(op.error());
1,967!
487
        auto val = parse_value();
1,967!
488
        if (!val) return dftracer::utils::unexpected(val.error());
1,968!
489
        return make_node(CompareNode{std::move(field), *op, std::move(*val)});
2,934!
490
    }
2,137✔
491

492
    FieldNode parse_field() {
2,144✔
493
        auto& tok = advance();
2,144✔
494
        return FieldNode{std::string(tok.text)};
2,142!
495
    }
496

497
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_match(
50✔
498
        FieldNode field, MatchOp op, bool negated) {
499
        if (current().kind != TokenKind::STRING) {
50✔
NEW
500
            return dftracer::utils::unexpected(
×
NEW
501
                error("Expected string pattern, got '" +
×
NEW
502
                      std::string(current().text) + "'"));
×
503
        }
504
        auto& tok = advance();
50✔
505
        std::string pattern(tok.text);
50!
506
        auto compiled = compile_pattern(op, pattern);
50!
507
        if (!compiled) {
50✔
508
            return dftracer::utils::unexpected(make_error(
4!
509
                source_, tok.column, tok.text.empty() ? 1 : tok.text.size(),
3✔
510
                "Invalid pattern: " + compiled.error()));
4!
511
        }
512
        MatchNode node;
48✔
513
        node.field = std::move(field);
48✔
514
        node.op = op;
48✔
515
        node.pattern = std::move(pattern);
48✔
516
        node.negated = negated;
48✔
517
        node.compiled = std::move(*compiled);
48!
518
        return make_node(std::move(node));
72!
519
    }
50✔
520

521
    // 'literal' in field / 'literal' not in field (case-insensitive substring).
522
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_contains() {
24✔
523
        auto& str_tok = advance();  // the string literal
24✔
524
        std::string literal(str_tok.text);
24!
525

526
        bool negated = false;
24✔
527
        if (current().kind == TokenKind::KW_NOT) {
24✔
528
            if (pos_ + 1 < tokens_.size() &&
9!
529
                tokens_[pos_ + 1].kind == TokenKind::KW_IN) {
6!
530
                advance();  // "not"
6!
531
                advance();  // "in"
6!
532
                negated = true;
6✔
533
            } else {
3✔
NEW
534
                return dftracer::utils::unexpected(
×
NEW
535
                    error("Expected 'in' after 'not'"));
×
536
            }
537
        } else if (current().kind == TokenKind::KW_IN) {
21!
538
            advance();  // "in"
18!
539
        } else {
9✔
NEW
540
            return dftracer::utils::unexpected(
×
NEW
541
                error("Expected 'in' after string literal, got '" +
×
NEW
542
                      std::string(current().text) + "'"));
×
543
        }
544

545
        if (current().kind != TokenKind::IDENT) {
24!
NEW
546
            return dftracer::utils::unexpected(
×
NEW
547
                error("Expected field name after 'in', got '" +
×
NEW
548
                      std::string(current().text) + "'"));
×
549
        }
550
        auto field = parse_field();
24!
551

552
        auto compiled = compile_pattern(MatchOp::ICONTAINS, literal);
24!
553
        if (!compiled) {
24✔
NEW
554
            return dftracer::utils::unexpected(
×
NEW
555
                make_error(source_, str_tok.column,
×
NEW
556
                           str_tok.text.empty() ? 1 : str_tok.text.size(),
×
NEW
557
                           "Invalid pattern: " + compiled.error()));
×
558
        }
559
        MatchNode node;
24✔
560
        node.field = std::move(field);
24✔
561
        node.op = MatchOp::ICONTAINS;
24✔
562
        node.pattern = std::move(literal);
24✔
563
        node.negated = negated;
24✔
564
        node.compiled = std::move(*compiled);
24!
565
        return make_node(std::move(node));
36!
566
    }
24✔
567

568
    dftracer::utils::expected<CompareOp, QueryError> parse_comp_op() {
1,969✔
569
        auto& tok = current();
1,969✔
570
        switch (tok.kind) {
1,967✔
571
            case TokenKind::OP_EQ:
808✔
572
                advance();
1,616✔
573
                return CompareOp::EQ;
1,615!
574
            case TokenKind::OP_NE:
3✔
575
                advance();
6✔
576
                return CompareOp::NE;
6!
577
            case TokenKind::OP_GT:
19✔
578
                advance();
38✔
579
                return CompareOp::GT;
38!
580
            case TokenKind::OP_LT:
1✔
581
                advance();
2✔
582
                return CompareOp::LT;
2!
583
            case TokenKind::OP_GE:
80✔
584
                advance();
163✔
585
                return CompareOp::GE;
165!
586
            case TokenKind::OP_LE:
71✔
587
                advance();
142✔
588
                return CompareOp::LE;
141!
589
            default:
590
                return dftracer::utils::unexpected(
×
591
                    error("Expected comparison operator (==, !=, >, "
×
592
                          "<, >=, <=), got '" +
×
593
                          std::string(tok.text) + "'"));
×
594
        }
595
    }
985✔
596

597
    dftracer::utils::expected<LiteralNode, QueryError> parse_value() {
2,439✔
598
        auto& tok = current();
2,439✔
599
        switch (tok.kind) {
2,440✔
600
            case TokenKind::STRING: {
1,005✔
601
                auto text = std::string(tok.text);
2,010!
602
                advance();
2,010!
603
                return LiteralNode{std::move(text)};
3,013!
604
            }
2,009✔
605
            case TokenKind::INT: {
206✔
606
                int64_t val = 0;
413✔
607
                auto [ptr, ec] = std::from_chars(
413!
608
                    tok.text.data(), tok.text.data() + tok.text.size(), val);
413✔
609
                if (ec != std::errc{}) {
413!
610
                    return dftracer::utils::unexpected(error(
×
611
                        "Invalid integer: '" + std::string(tok.text) + "'"));
×
612
                }
613
                advance();
413✔
614
                if (val >= 0) {
413✔
615
                    return LiteralNode{static_cast<uint64_t>(val)};
616!
616
                }
617
                return LiteralNode{val};
3!
618
            }
619
            case TokenKind::FLOAT: {
2✔
620
                double val = 0;
4✔
621
                auto sv = tok.text;
4✔
622
                // from_chars for double not available on all
623
                // compilers; use stod
624
                try {
625
                    val = std::stod(std::string(sv));
4!
626
                } catch (...) {
2✔
627
                    return dftracer::utils::unexpected(
×
628
                        error("Invalid float: '" + std::string(sv) + "'"));
×
629
                }
×
630
                advance();
4✔
631
                return LiteralNode{val};
6!
632
            }
633
            case TokenKind::KW_TRUE:
1✔
634
                advance();
2✔
635
                return LiteralNode{true};
3!
636
            case TokenKind::KW_FALSE:
1✔
637
                advance();
2✔
638
                return LiteralNode{false};
3!
639
            default:
4✔
640
                return dftracer::utils::unexpected(
15!
641
                    error("Expected value (string, number, or bool), "
15!
642
                          "got '" +
10!
643
                          std::string(tok.text) + "'"));
24!
644
        }
645
    }
1,221✔
646

647
    dftracer::utils::expected<ArrayNode, QueryError> parse_array() {
102✔
648
        if (!match(TokenKind::LBRACKET)) {
102!
649
            return dftracer::utils::unexpected(error("Expected '['"));
×
650
        }
651
        ArrayNode arr;
102✔
652
        if (current().kind != TokenKind::RBRACKET) {
102✔
653
            auto val = parse_value();
100!
654
            if (!val) return dftracer::utils::unexpected(val.error());
100!
655
            arr.elements.push_back(std::move(*val));
100!
656
            while (match(TokenKind::COMMA)) {
472✔
657
                val = parse_value();
372!
658
                if (!val) return dftracer::utils::unexpected(val.error());
372!
659
                arr.elements.push_back(std::move(*val));
372!
660
            }
661
        }
100✔
662
        if (!match(TokenKind::RBRACKET)) {
102!
663
            return dftracer::utils::unexpected(error("Expected ']' or ','"));
×
664
        }
665
        return arr;
102!
666
    }
102✔
667
};
668

669
}  // namespace
670

671
dftracer::utils::expected<QueryNodePtr, QueryError> parse_tokens(
1,424✔
672
    const std::vector<Token>& tokens, std::string_view source) {
673
    Parser parser(tokens, source);
1,424✔
674
    return parser.parse_query();
2,133!
675
}
676

677
dftracer::utils::expected<QueryNodePtr, QueryError> parse(
1,425✔
678
    std::string_view input) {
679
    auto tokens = tokenize(input);
1,425!
680
    if (!tokens) return dftracer::utils::unexpected(tokens.error());
1,424!
681
    return parse_tokens(*tokens, input);
1,420!
682
}
1,424✔
683

684
}  // namespace dftracer::utils::utilities::common::query
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc