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

llnl / dftracer-utils / 30067416336

24 Jul 2026 04:40AM UTC coverage: 50.693% (-2.0%) from 52.66%
30067416336

Pull #99

github

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

18063 of 48203 branches covered (37.47%)

Branch coverage included in aggregate %.

1514 of 2204 new or added lines in 58 files covered. (68.69%)

741 existing lines in 114 files now uncovered.

23715 of 34210 relevant lines covered (69.32%)

39842.55 hits per line

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

68.9
/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 {
2✔
15
    std::ostringstream os;
2!
16
    os << "Query parse error at column " << column << ":\n";
2!
17
    os << "  " << source << '\n';
2!
18
    os << "  " << indicator << '\n';
2!
19
    os << "  " << message << '\n';
2!
20
    return os.str();
4!
21
}
2✔
22

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

26
namespace {
27

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

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

36
bool iequals(std::string_view a, std::string_view b) {
7,521✔
37
    if (a.size() != b.size()) return false;
7,521✔
38
    return std::equal(a.begin(), a.end(), b.begin(),
2,022✔
39
                      [](unsigned char ca, unsigned char cb) {
2,585✔
40
                          return std::tolower(ca) == std::tolower(cb);
2,585✔
41
                      });
2,022✔
42
}
43

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

56
}  // namespace
57

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

63
    auto skip_ws = [&]() {
3,882✔
64
        while (pos < input.size() &&
11,958✔
65
               std::isspace(static_cast<unsigned char>(input[pos]))) {
5,684✔
66
            ++pos;
2,394✔
67
        }
68
    };
596✔
69

70
    while (true) {
71
        skip_ws();
3,883✔
72
        if (pos >= input.size()) break;
3,882✔
73

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

77
        // Operators
78
        if (c == '=' && pos + 1 < input.size() && input[pos + 1] == '=') {
3,287!
79
            tokens.push_back({TokenKind::OP_EQ, input.substr(start, 2), start});
582!
80
            pos += 2;
581✔
81
            continue;
581✔
82
        }
83
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '=') {
2,707!
84
            tokens.push_back({TokenKind::OP_NE, input.substr(start, 2), start});
4!
85
            pos += 2;
4✔
86
            continue;
4✔
87
        }
88
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '~') {
2,703!
89
            if (pos + 2 < input.size() && input[pos + 2] == '*') {
5!
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(
5!
95
                    {TokenKind::OP_NREGEX, input.substr(start, 2), start});
5!
96
                pos += 2;
5✔
97
            }
98
            continue;
5✔
99
        }
100
        if (c == '>' && pos + 1 < input.size() && input[pos + 1] == '=') {
2,697!
101
            tokens.push_back({TokenKind::OP_GE, input.substr(start, 2), start});
82!
102
            pos += 2;
82✔
103
            continue;
82✔
104
        }
105
        if (c == '<' && pos + 1 < input.size() && input[pos + 1] == '=') {
2,615✔
106
            tokens.push_back({TokenKind::OP_LE, input.substr(start, 2), start});
58!
107
            pos += 2;
58✔
108
            continue;
58✔
109
        }
110
        if (c == '>') {
2,558✔
111
            tokens.push_back({TokenKind::OP_GT, input.substr(start, 1), start});
23!
112
            ++pos;
23✔
113
            continue;
23✔
114
        }
115
        if (c == '<') {
2,535✔
116
            tokens.push_back({TokenKind::OP_LT, input.substr(start, 1), start});
2!
117
            ++pos;
2✔
118
            continue;
2✔
119
        }
120
        if (c == '~') {
2,533✔
121
            if (pos + 1 < input.size() && input[pos + 1] == '*') {
13!
122
                tokens.push_back(
4!
123
                    {TokenKind::OP_IREGEX, input.substr(start, 2), start});
4!
124
                pos += 2;
4✔
125
            } else {
126
                tokens.push_back(
9!
127
                    {TokenKind::OP_REGEX, input.substr(start, 1), start});
9!
128
                ++pos;
9✔
129
            }
130
            continue;
13✔
131
        }
132

133
        // Punctuation
134
        if (c == '(') {
2,520✔
135
            tokens.push_back(
5!
136
                {TokenKind::LPAREN, input.substr(start, 1), start});
5!
137
            ++pos;
5✔
138
            continue;
5✔
139
        }
140
        if (c == ')') {
2,515✔
141
            tokens.push_back(
5!
142
                {TokenKind::RPAREN, input.substr(start, 1), start});
5!
143
            ++pos;
5✔
144
            continue;
5✔
145
        }
146
        if (c == '[') {
2,510✔
147
            tokens.push_back(
52!
148
                {TokenKind::LBRACKET, input.substr(start, 1), start});
52!
149
            ++pos;
52✔
150
            continue;
52✔
151
        }
152
        if (c == ']') {
2,458✔
153
            tokens.push_back(
52!
154
                {TokenKind::RBRACKET, input.substr(start, 1), start});
52!
155
            ++pos;
52✔
156
            continue;
52✔
157
        }
158
        if (c == ',') {
2,406✔
159
            tokens.push_back({TokenKind::COMMA, input.substr(start, 1), start});
187!
160
            ++pos;
187✔
161
            continue;
187✔
162
        }
163

164
        // Strings
165
        if (c == '"' || c == '\'') {
2,219✔
166
            char quote = c;
811✔
167
            ++pos;
811✔
168
            while (pos < input.size() && input[pos] != quote) {
5,293✔
169
                if (input[pos] == '\\' && pos + 1 < input.size()) {
4,485!
170
                    pos += 2;
2✔
171
                } else {
172
                    ++pos;
4,480✔
173
                }
174
            }
175
            if (pos >= input.size()) {
812✔
176
                return dftracer::utils::unexpected(make_error(
2!
177
                    input, start, pos - start, "Unterminated string literal"));
1✔
178
            }
179
            ++pos;  // consume closing quote
810✔
180
            tokens.push_back({TokenKind::STRING,
809!
181
                              input.substr(start + 1, pos - start - 2), start});
810!
182
            continue;
809✔
183
        }
809✔
184

185
        // Numbers (including negative)
186
        if (std::isdigit(static_cast<unsigned char>(c)) ||
1,408✔
187
            (c == '-' && pos + 1 < input.size() &&
2!
188
             std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
4!
189
            bool is_float = false;
212✔
190
            ++pos;
212✔
191
            while (pos < input.size() &&
2,589✔
192
                   (std::isdigit(static_cast<unsigned char>(input[pos])) ||
1,239✔
193
                    input[pos] == '.' || input[pos] == 'e' ||
105✔
194
                    input[pos] == 'E' || input[pos] == '+' ||
101!
195
                    input[pos] == '-')) {
101!
196
                if (input[pos] == '.' || input[pos] == 'e' ||
2,272✔
197
                    input[pos] == 'E') {
1,134!
198
                    is_float = true;
4✔
199
                }
200
                ++pos;
1,138✔
201
            }
202
            auto text = input.substr(start, pos - start);
212!
203
            tokens.push_back(
212!
204
                {is_float ? TokenKind::FLOAT : TokenKind::INT, text, start});
212✔
205
            continue;
212✔
206
        }
212✔
207

208
        // Identifiers and keywords
209
        if (is_ident_start(c)) {
1,194✔
210
            ++pos;
1,195✔
211
            while (pos < input.size() && is_ident_char(input[pos])) {
3,594✔
212
                ++pos;
2,399✔
213
            }
214
            auto text = input.substr(start, pos - start);
1,197!
215
            TokenKind kind = TokenKind::IDENT;
1,196✔
216
            if (iequals(text, "and"))
1,196!
217
                kind = TokenKind::KW_AND;
136✔
218
            else if (iequals(text, "or"))
1,058!
219
                kind = TokenKind::KW_OR;
115✔
220
            else if (iequals(text, "not"))
944!
221
                kind = TokenKind::KW_NOT;
18✔
222
            else if (iequals(text, "in"))
927!
223
                kind = TokenKind::KW_IN;
64✔
224
            else if (iequals(text, "like"))
862!
225
                kind = TokenKind::KW_LIKE;
7✔
226
            else if (iequals(text, "ilike"))
853!
227
                kind = TokenKind::KW_ILIKE;
5✔
228
            else if (iequals(text, "true"))
849!
229
                kind = TokenKind::KW_TRUE;
2✔
230
            else if (iequals(text, "false"))
846!
231
                kind = TokenKind::KW_FALSE;
2✔
232
            tokens.push_back({kind, text, start});
1,193!
233
            continue;
1,197✔
234
        }
1,197✔
235

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

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

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

249
namespace {
250

251
void append_escaped(std::string& out, char c) {
87✔
252
    switch (c) {
87!
NEW
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;
87✔
271
    }
272
}
87✔
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) {
10✔
277
    std::string out = "^";
10!
278
    for (char c : like) {
62✔
279
        if (c == '%') {
52✔
280
            out += ".*";
16!
281
        } else if (c == '_') {
36✔
282
            out += '.';
2!
283
        } else {
284
            append_escaped(out, c);
34!
285
        }
286
    }
287
    out += '$';
10!
288
    return out;
10✔
NEW
289
}
×
290

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

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

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

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

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

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

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

356
    bool match(TokenKind kind) {
1,161✔
357
        if (current().kind == kind) {
1,161✔
358
            ++pos_;
298✔
359
            return true;
298✔
360
        }
361
        return false;
863✔
362
    }
363

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

370
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_or_expr() {
582✔
371
        auto left = parse_and_expr();
582!
372
        if (!left) return left;
583✔
373
        while (current().kind == TokenKind::KW_OR) {
689✔
374
            advance();
113✔
375
            auto right = parse_and_expr();
113!
376
            if (!right) return right;
113!
377
            left = make_node(OrNode{std::move(*left), std::move(*right)});
113!
378
        }
111!
379
        return left;
576✔
380
    }
582✔
381

382
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_and_expr() {
696✔
383
        auto left = parse_not_expr();
696!
384
        if (!left) return left;
694✔
385
        while (current().kind == TokenKind::KW_AND) {
813✔
386
            advance();
126✔
387
            auto right = parse_not_expr();
126!
388
            if (!right) return right;
126!
389
            left = make_node(AndNode{std::move(*left), std::move(*right)});
126!
390
        }
126!
391
        return left;
688✔
392
    }
694✔
393

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

404
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_primary() {
820✔
405
        if (match(TokenKind::LPAREN)) {
820✔
406
            auto expr = parse_or_expr();
5!
407
            if (!expr) return expr;
5!
408
            if (!match(TokenKind::RPAREN)) {
5!
409
                return dftracer::utils::unexpected(error("Expected ')'"));
×
410
            }
411
            return expr;
5✔
412
        }
5✔
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) {
813✔
418
            return parse_contains();
12!
419
        }
420

421
        if (current().kind != TokenKind::IDENT) {
802!
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();
804!
428

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

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

484
        // Comparison
485
        auto op = parse_comp_op();
728!
486
        if (!op) return dftracer::utils::unexpected(op.error());
726!
487
        auto val = parse_value();
726!
488
        if (!val) return dftracer::utils::unexpected(val.error());
727!
489
        return make_node(CompareNode{std::move(field), *op, std::move(*val)});
1,445!
490
    }
805✔
491

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

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

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

526
        bool negated = false;
12✔
527
        if (current().kind == TokenKind::KW_NOT) {
12✔
528
            if (pos_ + 1 < tokens_.size() &&
6!
529
                tokens_[pos_ + 1].kind == TokenKind::KW_IN) {
3!
530
                advance();  // "not"
3✔
531
                advance();  // "in"
3✔
532
                negated = true;
3✔
533
            } else {
NEW
534
                return dftracer::utils::unexpected(
×
NEW
535
                    error("Expected 'in' after 'not'"));
×
536
            }
537
        } else if (current().kind == TokenKind::KW_IN) {
9!
538
            advance();  // "in"
9✔
539
        } else {
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) {
12!
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();
12!
551

552
        auto compiled = compile_pattern(MatchOp::ICONTAINS, literal);
12!
553
        if (!compiled) {
12!
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;
12✔
560
        node.field = std::move(field);
12✔
561
        node.op = MatchOp::ICONTAINS;
12✔
562
        node.pattern = std::move(literal);
12✔
563
        node.negated = negated;
12✔
564
        node.compiled = std::move(*compiled);
12!
565
        return make_node(std::move(node));
24!
566
    }
12✔
567

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

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

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

669
}  // namespace
670

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

677
dftracer::utils::expected<QueryNodePtr, QueryError> parse(
578✔
678
    std::string_view input) {
679
    auto tokens = tokenize(input);
578!
680
    if (!tokens) return dftracer::utils::unexpected(tokens.error());
578!
681
    return parse_tokens(*tokens, input);
575!
682
}
578✔
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