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

tstack / lnav / 21043099841-2763

15 Jan 2026 06:11AM UTC coverage: 68.949% (+0.02%) from 68.934%
21043099841-2763

push

github

tstack
[logfmt] parse spans that are not kv-pairs

84 of 94 new or added lines in 5 files covered. (89.36%)

2 existing lines in 1 file now uncovered.

51802 of 75131 relevant lines covered (68.95%)

434450.69 hits per line

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

85.83
/src/string-extension-functions.cc
1
/*
2
 * Written by Alexey Tourbin <at@altlinux.org>.
3
 *
4
 * The author has dedicated the code to the public domain.  Anyone is free
5
 * to copy, modify, publish, use, compile, sell, or distribute the original
6
 * code, either in source code form or as a compiled binary, for any purpose,
7
 * commercial or non-commercial, and by any means.
8
 */
9

10
#include <unordered_map>
11

12
#include <sqlite3.h>
13
#include <stdlib.h>
14
#include <string.h>
15

16
#include "base/fts_fuzzy_match.hh"
17
#include "base/humanize.hh"
18
#include "base/is_utf8.hh"
19
#include "base/lnav.gzip.hh"
20
#include "base/string_util.hh"
21
#include "column_namer.hh"
22
#include "config.h"
23
#include "data_parser.hh"
24
#include "data_scanner.hh"
25
#include "elem_to_json.hh"
26
#include "fmt/format.h"
27
#include "formats/logfmt/logfmt.parser.hh"
28
#include "hasher.hh"
29
#include "libbase64.h"
30
#include "mapbox/variant.hpp"
31
#include "md4cpp.hh"
32
#include "pcrepp/pcre2pp.hh"
33
#include "pretty_printer.hh"
34
#include "safe/safe.h"
35
#include "scn/scan.h"
36
#include "sqlite-extension-func.hh"
37
#include "text_anonymizer.hh"
38
#include "view_curses.hh"
39
#include "vtab_module.hh"
40
#include "vtab_module_json.hh"
41
#include "yajl/api/yajl_gen.h"
42
#include "yajlpp/json_op.hh"
43
#include "yajlpp/yajlpp.hh"
44
#include "yajlpp/yajlpp_def.hh"
45

46
#if defined(HAVE_LIBCURL)
47
#    include <curl/curl.h>
48
#endif
49

50
enum class encode_algo {
51
    base64,
52
    hex,
53
    uri,
54
    html,
55
};
56

57
template<>
58
struct from_sqlite<encode_algo> {
59
    encode_algo operator()(int argc, sqlite3_value** val, int argi)
31✔
60
    {
61
        const char* algo_name = (const char*) sqlite3_value_text(val[argi]);
31✔
62

63
        if (strcasecmp(algo_name, "base64") == 0) {
31✔
64
            return encode_algo::base64;
8✔
65
        }
66
        if (strcasecmp(algo_name, "hex") == 0) {
23✔
67
            return encode_algo::hex;
10✔
68
        }
69
        if (strcasecmp(algo_name, "uri") == 0) {
13✔
70
            return encode_algo::uri;
10✔
71
        }
72
        if (strcasecmp(algo_name, "html") == 0) {
3✔
73
            return encode_algo::html;
2✔
74
        }
75

76
        throw from_sqlite_conversion_error(
77
            "value of 'base64', 'hex', 'uri', or 'html'", argi);
1✔
78
    }
79
};
80

81
namespace {
82

83
struct cache_entry {
84
    std::shared_ptr<lnav::pcre2pp::code> re2;
85
    std::shared_ptr<column_namer> cn{
86
        std::make_shared<column_namer>(column_namer::language::JSON)};
87
};
88

89
cache_entry*
90
find_re(string_fragment re)
2,919✔
91
{
92
    using re_cache_t
93
        = std::unordered_map<string_fragment, cache_entry, frag_hasher>;
94
    thread_local re_cache_t cache;
2,919✔
95

96
    auto iter = cache.find(re);
2,919✔
97
    if (iter == cache.end()) {
2,919✔
98
        auto compile_res = lnav::pcre2pp::code::from(re);
2,424✔
99
        if (compile_res.isErr()) {
2,424✔
100
            const static intern_string_t SRC = intern_string::lookup("arg");
3✔
101

102
            throw lnav::console::to_user_message(SRC, compile_res.unwrapErr());
1✔
103
        }
104

105
        cache_entry c;
2,423✔
106

107
        c.re2 = compile_res.unwrap().to_shared();
2,423✔
108
        auto pair = cache.insert(
2,423✔
109
            std::make_pair(string_fragment::from_str(c.re2->get_pattern()), c));
4,846✔
110

111
        for (size_t lpc = 0; lpc < c.re2->get_capture_count(); lpc++) {
2,479✔
112
            c.cn->add_column(string_fragment::from_c_str(
56✔
113
                c.re2->get_name_for_capture(lpc + 1)));
114
        }
115

116
        iter = pair.first;
2,423✔
117
    }
2,424✔
118

119
    return &iter->second;
5,836✔
120
}
121

122
bool
123
regexp(string_fragment re, string_fragment str)
2,376✔
124
{
125
    auto* reobj = find_re(re);
2,376✔
126

127
    return reobj->re2->find_in(str).ignore_error().has_value();
2,376✔
128
}
129

130
mapbox::util::
131
    variant<int64_t, double, const char*, string_fragment, json_string>
132
    regexp_match(string_fragment re, string_fragment str)
517✔
133
{
134
    auto* reobj = find_re(re);
517✔
135
    auto& extractor = *reobj->re2;
516✔
136

137
    if (extractor.get_capture_count() == 0) {
516✔
138
        throw std::runtime_error(
139
            "regular expression does not have any captures");
1✔
140
    }
141

142
    auto md = extractor.create_match_data();
515✔
143
    auto match_res = extractor.capture_from(str).into(md).matches();
515✔
144
    if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
515✔
145
        return static_cast<const char*>(nullptr);
1✔
146
    }
147
    if (match_res.is<lnav::pcre2pp::matcher::error>()) {
514✔
148
        auto err = match_res.get<lnav::pcre2pp::matcher::error>();
×
149

150
        throw std::runtime_error(err.get_message());
×
151
    }
152

153
    if (extractor.get_capture_count() == 1) {
514✔
154
        auto cap = md[1];
500✔
155

156
        if (!cap) {
500✔
157
            return static_cast<const char*>(nullptr);
×
158
        }
159

160
        auto scan_int_res = scn::scan_int<int64_t>(cap->to_string_view());
500✔
161
        if (scan_int_res) {
500✔
162
            if (scan_int_res->range().empty()) {
15✔
163
                return scan_int_res->value();
15✔
164
            }
165
            auto scan_float_res
166
                = scn::scan_value<double>(cap->to_string_view());
1✔
167
            if (scan_float_res && scan_float_res->range().empty()) {
1✔
168
                return scan_float_res->value();
1✔
169
            }
170
        }
171

172
        return cap.value();
485✔
173
    }
174

175
    yajlpp_gen gen;
14✔
176
    yajl_gen_config(gen, yajl_gen_beautify, false);
14✔
177
    {
178
        yajlpp_map root_map(gen);
14✔
179

180
        for (size_t lpc = 0; lpc < extractor.get_capture_count(); lpc++) {
42✔
181
            const auto& colname = reobj->cn->cn_names[lpc];
28✔
182
            const auto cap = md[lpc + 1];
28✔
183

184
            yajl_gen_pstring(gen, colname.data(), colname.length());
28✔
185

186
            if (!cap) {
28✔
187
                yajl_gen_null(gen);
×
188
            } else {
189
                auto scan_int_res
190
                    = scn::scan_value<int64_t>(cap->to_string_view());
28✔
191
                if (scan_int_res && scan_int_res->range().empty()) {
28✔
192
                    yajl_gen_integer(gen, scan_int_res->value());
11✔
193
                } else {
194
                    auto scan_float_res
195
                        = scn::scan_value<double>(cap->to_string_view());
17✔
196
                    if (scan_float_res && scan_float_res->range().empty()) {
17✔
197
                        yajl_gen_number(gen, cap->data(), cap->length());
1✔
198
                    } else {
199
                        yajl_gen_pstring(gen, cap->data(), cap->length());
16✔
200
                    }
201
                }
202
            }
203
        }
204
    }
14✔
205

206
    return json_string(gen);
14✔
207
#if 0
208
    sqlite3_result_text(ctx, (const char *) buf, len, SQLITE_TRANSIENT);
209
#    ifdef HAVE_SQLITE3_VALUE_SUBTYPE
210
    sqlite3_result_subtype(ctx, JSON_SUBTYPE);
211
#    endif
212
#endif
213
}
515✔
214

215
static json_string
216
logfmt2json(string_fragment line)
6✔
217
{
218
    std::vector<string_fragment> unbound;
6✔
219
    logfmt::parser p(line);
6✔
220
    yajlpp_gen gen;
6✔
221
    yajl_gen_config(gen, yajl_gen_beautify, false);
6✔
222
    {
223
        yajlpp_map root(gen);
6✔
224
        bool done = false;
6✔
225

226
        while (!done) {
31✔
227
            auto pair = p.step();
25✔
228

229
            done = pair.match(
50✔
230
                [](const logfmt::parser::end_of_input& eoi) { return true; },
6✔
NEW
231
                [&unbound](const string_fragment& sf) {
×
NEW
232
                    unbound.emplace_back(sf);
×
NEW
233
                    return false;
×
234
                },
235
                [&root, &gen](const logfmt::parser::kvpair& kvp) {
25✔
236
                    root.gen(kvp.first);
19✔
237

238
                    kvp.second.match(
19✔
239
                        [&root](const logfmt::parser::bool_value& bv) {
×
240
                            root.gen(bv.bv_value);
×
241
                        },
×
242
                        [&root](const logfmt::parser::int_value& iv) {
×
243
                            root.gen(iv.iv_value);
12✔
244
                        },
12✔
245
                        [&root](const logfmt::parser::float_value& fv) {
×
246
                            root.gen(fv.fv_value);
1✔
247
                        },
1✔
248
                        [&root, &gen](const logfmt::parser::quoted_value& qv) {
×
249
                            auto_mem<yajl_handle_t> parse_handle(yajl_free);
5✔
250
                            json_ptr jp("");
5✔
251
                            json_op jo(jp);
5✔
252

253
                            jo.jo_ptr_callbacks = json_op::gen_callbacks;
5✔
254
                            jo.jo_ptr_data = gen;
5✔
255
                            parse_handle.reset(yajl_alloc(
5✔
256
                                &json_op::ptr_callbacks, nullptr, &jo));
257

258
                            const auto* json_in
259
                                = (const unsigned char*) qv.qv_value.data();
5✔
260
                            auto json_len = qv.qv_value.length();
5✔
261

262
                            if (yajl_parse(parse_handle.in(), json_in, json_len)
5✔
263
                                    != yajl_status_ok
264
                                || yajl_complete_parse(parse_handle.in())
5✔
265
                                    != yajl_status_ok)
266
                            {
267
                                root.gen(qv.qv_value);
×
268
                            }
269
                        },
5✔
270
                        [&root](const logfmt::parser::unquoted_value& uv) {
19✔
271
                            root.gen(uv.uv_value);
1✔
272
                        });
1✔
273

274
                    return false;
19✔
275
                },
276
                [](const logfmt::parser::error& e) -> bool {
×
277
                    throw sqlite_func_error("Invalid logfmt: {}", e.e_msg);
×
278
                });
279
        }
25✔
280

281
        if (!unbound.empty()) {
6✔
NEW
282
            root.gen("__unbound__");
×
NEW
283
            yajlpp_array unbound_array(gen);
×
NEW
284
            for (const auto& sf : unbound) {
×
NEW
285
                unbound_array.gen(sf);
×
286
            }
287
        }
288
    }
6✔
289

290
    return json_string(gen);
12✔
291
}
6✔
292

293
std::string
294
regexp_replace(string_fragment str, string_fragment re, const char* repl)
26✔
295
{
296
    auto* reobj = find_re(re);
26✔
297

298
    return reobj->re2->replace(str, repl);
26✔
299
}
300

301
std::optional<int64_t>
302
sql_fuzzy_match(const char* pat, const char* str)
5✔
303
{
304
    if (pat == nullptr) {
5✔
305
        return std::nullopt;
×
306
    }
307

308
    if (pat[0] == '\0') {
5✔
309
        return 1;
×
310
    }
311

312
    int score = 0;
5✔
313

314
    if (!fts::fuzzy_match(pat, str, score)) {
5✔
315
        return std::nullopt;
×
316
    }
317

318
    return score;
5✔
319
}
320

321
string_fragment
322
spooky_hash(const std::vector<const char*>& args)
5,527✔
323
{
324
    thread_local char hash_str_buf[hasher::STRING_SIZE];
325

326
    hasher context;
5,527✔
327
    for (const auto* const arg : args) {
16,576✔
328
        int64_t len = arg != nullptr ? strlen(arg) : 0;
11,049✔
329

330
        context.update((const char*) &len, sizeof(len));
11,049✔
331
        if (arg == nullptr) {
11,049✔
332
            continue;
30✔
333
        }
334
        context.update(arg, len);
11,019✔
335
    }
336
    context.to_string(hash_str_buf);
5,527✔
337

338
    return string_fragment::from_bytes(hash_str_buf, sizeof(hash_str_buf) - 1);
11,054✔
339
}
340

341
void
342
sql_spooky_hash_step(sqlite3_context* context, int argc, sqlite3_value** argv)
10✔
343
{
344
    auto* hasher
345
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
10✔
346

347
    for (int lpc = 0; lpc < argc; lpc++) {
20✔
348
        const auto* value = sqlite3_value_text(argv[lpc]);
10✔
349
        int64_t len = value != nullptr ? strlen((const char*) value) : 0;
10✔
350

351
        hasher->Update(&len, sizeof(len));
10✔
352
        if (value == nullptr) {
10✔
353
            continue;
×
354
        }
355
        hasher->Update(value, len);
10✔
356
    }
357
}
10✔
358

359
void
360
sql_spooky_hash_final(sqlite3_context* context)
5✔
361
{
362
    auto* hasher
363
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
5✔
364

365
    if (hasher == nullptr) {
5✔
366
        sqlite3_result_null(context);
×
367
    } else {
368
        byte_array<2, uint64> hash;
5✔
369

370
        hasher->Final(hash.out(0), hash.out(1));
5✔
371

372
        auto hex = hash.to_string();
5✔
373
        sqlite3_result_text(
5✔
374
            context, hex.c_str(), hex.length(), SQLITE_TRANSIENT);
5✔
375
    }
5✔
376
}
5✔
377

378
struct sparkline_context {
379
    bool sc_initialized{true};
380
    double sc_max_value{0.0};
381
    std::vector<double> sc_values;
382
};
383

384
void
385
sparkline_step(sqlite3_context* context, int argc, sqlite3_value** argv)
50✔
386
{
387
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
50✔
388
        context, sizeof(sparkline_context));
389

390
    if (!sc->sc_initialized) {
50✔
391
        new (sc) sparkline_context;
10✔
392
    }
393

394
    if (argc == 0) {
50✔
395
        return;
×
396
    }
397

398
    sc->sc_values.push_back(sqlite3_value_double(argv[0]));
50✔
399
    sc->sc_max_value = std::max(sc->sc_max_value, sc->sc_values.back());
50✔
400

401
    if (argc >= 2) {
50✔
402
        sc->sc_max_value
403
            = std::max(sc->sc_max_value, sqlite3_value_double(argv[1]));
5✔
404
    }
405
}
406

407
void
408
sparkline_final(sqlite3_context* context)
10✔
409
{
410
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
10✔
411
        context, sizeof(sparkline_context));
412

413
    if (!sc->sc_initialized) {
10✔
414
        sqlite3_result_text(context, "", 0, SQLITE_STATIC);
×
415
        return;
×
416
    }
417

418
    auto retval = auto_mem<char>::malloc(sc->sc_values.size() * 3 + 1);
10✔
419
    auto* start = retval.in();
10✔
420

421
    for (const auto& value : sc->sc_values) {
60✔
422
        auto bar = humanize::sparkline(value, sc->sc_max_value);
50✔
423

424
        strcpy(start, bar.c_str());
50✔
425
        start += bar.length();
50✔
426
    }
50✔
427
    *start = '\0';
10✔
428

429
    to_sqlite(context, std::move(retval));
10✔
430

431
    sc->~sparkline_context();
10✔
432
}
10✔
433

434
std::optional<mapbox::util::variant<blob_auto_buffer, sqlite3_int64, double>>
435
sql_gunzip(sqlite3_value* val)
2✔
436
{
437
    switch (sqlite3_value_type(val)) {
2✔
438
        case SQLITE3_TEXT:
2✔
439
        case SQLITE_BLOB: {
440
            const auto* buffer = sqlite3_value_blob(val);
2✔
441
            auto len = sqlite3_value_bytes(val);
2✔
442

443
            if (!lnav::gzip::is_gzipped((const char*) buffer, len)) {
2✔
444
                return blob_auto_buffer{
×
445
                    auto_buffer::from((const char*) buffer, len)};
×
446
            }
447

448
            auto res = lnav::gzip::uncompress("", buffer, len);
4✔
449

450
            if (res.isErr()) {
2✔
451
                throw sqlite_func_error("unable to uncompress -- {}",
452
                                        res.unwrapErr());
×
453
            }
454

455
            return blob_auto_buffer{res.unwrap()};
2✔
456
        }
2✔
457
        case SQLITE_INTEGER:
×
458
            return sqlite3_value_int64(val);
×
459
        case SQLITE_FLOAT:
×
460
            return sqlite3_value_double(val);
×
461
    }
462

463
    return std::nullopt;
×
464
}
465

466
std::optional<blob_auto_buffer>
467
sql_gzip(sqlite3_value* val)
3✔
468
{
469
    switch (sqlite3_value_type(val)) {
3✔
470
        case SQLITE3_TEXT:
1✔
471
        case SQLITE_BLOB: {
472
            const auto* buffer = sqlite3_value_blob(val);
1✔
473
            auto len = sqlite3_value_bytes(val);
1✔
474
            auto res = lnav::gzip::compress(buffer, len);
1✔
475

476
            if (res.isErr()) {
1✔
477
                throw sqlite_func_error("unable to compress -- {}",
478
                                        res.unwrapErr());
×
479
            }
480

481
            return blob_auto_buffer{res.unwrap()};
1✔
482
        }
1✔
483
        case SQLITE_INTEGER:
2✔
484
        case SQLITE_FLOAT: {
485
            const auto* buffer = sqlite3_value_text(val);
2✔
486
            auto res
487
                = lnav::gzip::compress(buffer, strlen((const char*) buffer));
2✔
488

489
            if (res.isErr()) {
2✔
490
                throw sqlite_func_error("unable to compress -- {}",
491
                                        res.unwrapErr());
×
492
            }
493

494
            return blob_auto_buffer{res.unwrap()};
2✔
495
        }
2✔
496
    }
497

498
    return std::nullopt;
×
499
}
500

501
#if defined(HAVE_LIBCURL)
502
static CURL*
503
get_curl_easy()
45✔
504
{
505
    static struct curl_wrapper {
506
        curl_wrapper() { this->cw_value = curl_easy_init(); }
26✔
507

508
        auto_mem<CURL> cw_value{curl_easy_cleanup};
509
    } retval;
45✔
510

511
    return retval.cw_value.in();
45✔
512
}
513
#endif
514

515
mapbox::util::variant<text_auto_buffer, auto_mem<char>, null_value_t>
516
sql_encode(sqlite3_value* value, encode_algo algo)
20✔
517
{
518
    switch (sqlite3_value_type(value)) {
20✔
519
        case SQLITE_NULL: {
1✔
520
            return null_value_t{};
1✔
521
        }
522
        case SQLITE_BLOB: {
1✔
523
            const auto* blob
524
                = static_cast<const char*>(sqlite3_value_blob(value));
1✔
525
            auto blob_len = sqlite3_value_bytes(value);
1✔
526

527
            switch (algo) {
1✔
528
                case encode_algo::base64: {
1✔
529
                    auto buf = auto_buffer::alloc((blob_len * 5) / 3);
1✔
530
                    auto outlen = buf.capacity();
1✔
531

532
                    base64_encode(blob, blob_len, buf.in(), &outlen, 0);
1✔
533
                    buf.resize(outlen);
1✔
534
                    return text_auto_buffer{std::move(buf)};
1✔
535
                }
1✔
536
                case encode_algo::hex: {
×
537
                    auto buf = auto_buffer::alloc(blob_len * 2 + 1);
×
538

539
                    for (int lpc = 0; lpc < blob_len; lpc++) {
×
540
                        fmt::format_to(std::back_inserter(buf),
×
541
                                       FMT_STRING("{:02x}"),
×
542
                                       blob[lpc]);
×
543
                    }
544

545
                    return text_auto_buffer{std::move(buf)};
×
546
                }
547
#if defined(HAVE_LIBCURL)
548
                case encode_algo::uri: {
×
549
                    auto_mem<char> retval(curl_free);
×
550

551
                    retval = curl_easy_escape(get_curl_easy(), blob, blob_len);
×
552
                    return std::move(retval);
×
553
                }
554
#endif
555
                case encode_algo::html: {
×
556
                    return md4cpp::escape_html(
×
557
                        string_fragment::from_bytes(blob, blob_len));
×
558
                }
559
            }
560
        }
561
        default: {
562
            const auto* text = (const char*) sqlite3_value_text(value);
18✔
563
            auto text_len = sqlite3_value_bytes(value);
18✔
564

565
            switch (algo) {
18✔
566
                case encode_algo::base64: {
5✔
567
                    auto buf = auto_buffer::alloc((text_len * 5) / 3);
5✔
568
                    size_t outlen = buf.capacity();
5✔
569

570
                    base64_encode(text, text_len, buf.in(), &outlen, 0);
5✔
571
                    buf.resize(outlen);
5✔
572
                    return text_auto_buffer{std::move(buf)};
5✔
573
                }
5✔
574
                case encode_algo::hex: {
7✔
575
                    auto buf = auto_buffer::alloc(text_len * 2 + 1);
7✔
576

577
                    for (int lpc = 0; lpc < text_len; lpc++) {
88✔
578
                        fmt::format_to(std::back_inserter(buf),
81✔
579
                                       FMT_STRING("{:02x}"),
243✔
580
                                       text[lpc]);
81✔
581
                    }
582

583
                    return text_auto_buffer{std::move(buf)};
7✔
584
                }
7✔
585
#if defined(HAVE_LIBCURL)
586
                case encode_algo::uri: {
5✔
587
                    auto_mem<char> retval(curl_free);
5✔
588

589
                    retval = curl_easy_escape(get_curl_easy(), text, text_len);
5✔
590
                    return std::move(retval);
5✔
591
                }
5✔
592
#endif
593
                case encode_algo::html: {
1✔
594
                    return md4cpp::escape_html(
2✔
595
                        string_fragment::from_bytes(text, text_len));
1✔
596
                }
597
            }
598
        }
599
    }
600
    ensure(false);
×
601
}
602

603
mapbox::util::variant<blob_auto_buffer, text_auto_buffer, auto_mem<char>>
604
sql_decode(string_fragment str, encode_algo algo)
10✔
605
{
606
    switch (algo) {
10✔
607
        case encode_algo::base64: {
1✔
608
            auto buf = auto_buffer::alloc(str.length());
1✔
609
            auto outlen = buf.capacity();
1✔
610
            base64_decode(str.data(), str.length(), buf.in(), &outlen, 0);
1✔
611
            buf.resize(outlen);
1✔
612

613
            return blob_auto_buffer{std::move(buf)};
1✔
614
        }
1✔
615
        case encode_algo::hex: {
3✔
616
            auto buf = auto_buffer::alloc(str.length() / 2);
3✔
617
            auto sv = str.to_string_view();
3✔
618

619
            if (sv.size() % 2 != 0) {
3✔
620
                throw sqlite_func_error(
621
                    "hex input is not a multiple of two characters");
2✔
622
            }
623
            while (sv.size() >= 2) {
16✔
624
                auto scan_res = scn::scan<uint8_t>(sv.substr(0, 2), "{:2x}");
15✔
625
                if (!scan_res) {
15✔
626
                    throw sqlite_func_error(
627
                        "invalid hex input at: {}",
628
                        std::distance(str.begin(), sv.begin()));
3✔
629
                }
630
                auto value = scan_res->value();
14✔
631
                buf.push_back((char) (value & 0xff));
14✔
632
                sv = sv.substr(2);
14✔
633
            }
634

635
            return blob_auto_buffer{std::move(buf)};
1✔
636
        }
3✔
637
#if defined(HAVE_LIBCURL)
638
        case encode_algo::uri: {
5✔
639
            auto_mem<char> retval(curl_free);
5✔
640

641
            retval = curl_easy_unescape(
642
                get_curl_easy(), str.data(), str.length(), nullptr);
5✔
643

644
            return std::move(retval);
5✔
645
        }
5✔
646
#endif
647
        case encode_algo::html: {
1✔
648
            static const auto ENTITY_RE = lnav::pcre2pp::code::from_const(
649
                R"(&(?:([a-z0-9]+)|#([0-9]{1,6})|#x([0-9a-fA-F]{1,6}));)",
650
                PCRE2_CASELESS);
1✔
651
            static const auto ENTITIES = md4cpp::get_xml_entity_map();
1✔
652

653
            auto buf = auto_buffer::alloc(str.length());
1✔
654
            auto res = ENTITY_RE.capture_from(str).for_each(
1✔
655
                [&buf](const auto& match) {
3✔
656
                    buf.append(match.leading().to_string_view());
3✔
657
                    auto named = match[1];
3✔
658
                    if (named) {
3✔
659
                        auto iter
660
                            = ENTITIES.xem_entities.find(match[0]->to_string());
1✔
661
                        if (iter != ENTITIES.xem_entities.end()) {
1✔
662
                            buf.append(iter->second.xe_chars);
1✔
663
                        } else {
664
                            buf.append(match[0]->to_string_view());
×
665
                        }
666
                        return;
1✔
667
                    }
668
                    auto dec = match[2];
2✔
669
                    if (dec) {
2✔
670
                        auto scan_res
1✔
671
                            = scn::scan_int<uint32_t>(dec->to_string_view());
672
                        if (scan_res) {
1✔
673
                            ww898::utf::utf8::write(
1✔
674
                                scan_res.value().value(),
1✔
675
                                [&buf](uint8_t c) { buf.push_back(c); });
3✔
676
                        } else {
677
                            buf.append(match[0]->to_string_view());
×
678
                        }
679
                        return;
1✔
680
                    }
681
                    auto hex = match[3];
1✔
682
                    if (hex) {
1✔
683
                        auto scan_res = scn::scan_int<uint32_t>(
1✔
684
                            hex->to_string_view(), 16);
685
                        if (scan_res) {
1✔
686
                            ww898::utf::utf8::write(
1✔
687
                                scan_res.value().value(),
1✔
688
                                [&buf](uint8_t c) { buf.push_back(c); });
3✔
689
                        } else {
690
                            buf.append(match[0]->to_string_view());
×
691
                        }
692
                        return;
1✔
693
                    }
694
                });
1✔
695
            if (res.isOk()) {
1✔
696
                auto remaining = res.unwrap();
1✔
697
                buf.append(remaining.to_string_view());
1✔
698
            }
699
            return text_auto_buffer{std::move(buf)};
1✔
700
        }
1✔
701
    }
702
    ensure(false);
×
703
}
704

705
std::string
706
sql_humanize_file_size(file_ssize_t value)
279✔
707
{
708
    return humanize::file_size(value, humanize::alignment::columnar);
279✔
709
}
710

711
std::string
712
sql_anonymize(string_fragment frag)
202✔
713
{
714
    static safe::Safe<lnav::text_anonymizer> ta;
202✔
715

716
    return ta.writeAccess()->next(frag);
202✔
717
}
718

719
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
720
extern "C"
721
{
722
const char* curl_url_strerror(CURLUcode error);
723
}
724
#endif
725

726
json_string
727
sql_parse_url(std::string url)
27✔
728
{
729
    static auto* CURL_HANDLE = get_curl_easy();
27✔
730

731
    auto_mem<CURLU> cu(curl_url_cleanup);
27✔
732
    cu = curl_url();
27✔
733

734
    auto rc = curl_url_set(
27✔
735
        cu, CURLUPART_URL, url.c_str(), CURLU_NON_SUPPORT_SCHEME);
736
    if (rc != CURLUE_OK) {
27✔
737
        auto_mem<char> url_part(curl_free);
1✔
738
        yajlpp_gen gen;
1✔
739
        yajl_gen_config(gen, yajl_gen_beautify, false);
1✔
740

741
        {
742
            yajlpp_map root(gen);
1✔
743
            root.gen("error");
1✔
744
            root.gen("invalid-url");
1✔
745
            root.gen("url");
1✔
746
            root.gen(url);
1✔
747
            root.gen("reason");
1✔
748
            root.gen(curl_url_strerror(rc));
1✔
749
        }
1✔
750

751
        return json_string(gen);
1✔
752
    }
1✔
753

754
    auto_mem<char> url_part(curl_free);
26✔
755
    yajlpp_gen gen;
26✔
756
    yajl_gen_config(gen, yajl_gen_beautify, false);
26✔
757

758
    {
759
        yajlpp_map root(gen);
26✔
760

761
        root.gen("scheme");
26✔
762
        rc = curl_url_get(cu, CURLUPART_SCHEME, url_part.out(), 0);
26✔
763
        if (rc == CURLUE_OK) {
26✔
764
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
765
        } else {
766
            root.gen();
×
767
        }
768
        root.gen("username");
26✔
769
        rc = curl_url_get(cu, CURLUPART_USER, url_part.out(), CURLU_URLDECODE);
26✔
770
        if (rc == CURLUE_OK) {
26✔
771
            root.gen(string_fragment::from_c_str(url_part.in()));
5✔
772
        } else {
773
            root.gen();
21✔
774
        }
775
        root.gen("password");
26✔
776
        rc = curl_url_get(
26✔
777
            cu, CURLUPART_PASSWORD, url_part.out(), CURLU_URLDECODE);
778
        if (rc == CURLUE_OK) {
26✔
779
            root.gen(string_fragment::from_c_str(url_part.in()));
×
780
        } else {
781
            root.gen();
26✔
782
        }
783
        root.gen("host");
26✔
784
        rc = curl_url_get(cu, CURLUPART_HOST, url_part.out(), CURLU_URLDECODE);
26✔
785
        if (rc == CURLUE_OK) {
26✔
786
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
787
        } else {
788
            root.gen();
×
789
        }
790
        root.gen("port");
26✔
791
        rc = curl_url_get(cu, CURLUPART_PORT, url_part.out(), 0);
26✔
792
        if (rc == CURLUE_OK) {
26✔
793
            root.gen(string_fragment::from_c_str(url_part.in()));
×
794
        } else {
795
            root.gen();
26✔
796
        }
797
        root.gen("path");
26✔
798
        rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE);
26✔
799
        if (rc == CURLUE_OK) {
26✔
800
            auto path_frag = string_fragment::from_c_str(url_part.in());
26✔
801
            auto path_utf_res = is_utf8(path_frag);
26✔
802
            if (path_utf_res.is_valid()) {
26✔
803
                root.gen(path_frag);
26✔
804
            } else {
805
                rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), 0);
×
806
                if (rc == CURLUE_OK) {
×
807
                    root.gen(string_fragment::from_c_str(url_part.in()));
×
808
                } else {
809
                    root.gen();
×
810
                }
811
            }
812
        } else {
813
            root.gen();
×
814
        }
815
        rc = curl_url_get(cu, CURLUPART_QUERY, url_part.out(), 0);
26✔
816
        if (rc == CURLUE_OK) {
26✔
817
            root.gen("query");
14✔
818
            root.gen(string_fragment::from_c_str(url_part.in()));
14✔
819

820
            root.gen("parameters");
14✔
821
            robin_hood::unordered_set<std::string> seen_keys;
14✔
822
            yajlpp_map query_map(gen);
14✔
823

824
            for (size_t lpc = 0; url_part.in()[lpc]; lpc++) {
206✔
825
                if (url_part.in()[lpc] == '+') {
192✔
826
                    url_part.in()[lpc] = ' ';
1✔
827
                }
828
            }
829
            auto query_frag = string_fragment::from_c_str(url_part.in());
14✔
830
            auto remaining = query_frag;
14✔
831

832
            while (true) {
833
                auto split_res
834
                    = remaining.split_when(string_fragment::tag1{'&'});
27✔
835
                auto_mem<char> kv_pair(curl_free);
27✔
836
                auto kv_pair_encoded = split_res.first;
27✔
837
                int out_len = 0;
27✔
838

839
                kv_pair = curl_easy_unescape(CURL_HANDLE,
840
                                             kv_pair_encoded.data(),
841
                                             kv_pair_encoded.length(),
842
                                             &out_len);
27✔
843

844
                auto kv_pair_frag
845
                    = string_fragment::from_bytes(kv_pair.in(), out_len);
27✔
846
                auto eq_index_opt = kv_pair_frag.find('=');
27✔
847
                if (eq_index_opt) {
27✔
848
                    auto key = kv_pair_frag.sub_range(0, eq_index_opt.value());
11✔
849
                    auto val = kv_pair_frag.substr(eq_index_opt.value() + 1);
11✔
850

851
                    auto key_utf_res = is_utf8(key);
11✔
852
                    auto val_utf_res = is_utf8(val);
11✔
853
                    if (key_utf_res.is_valid()) {
11✔
854
                        auto key_str = key.to_string();
11✔
855

856
                        if (seen_keys.count(key_str) == 0) {
11✔
857
                            seen_keys.emplace(key_str);
11✔
858
                            query_map.gen(key);
11✔
859
                            if (val_utf_res.is_valid()) {
11✔
860
                                query_map.gen(val);
11✔
861
                            } else {
862
                                auto eq = strchr(kv_pair_encoded.data(), '=');
×
863
                                query_map.gen(
×
864
                                    string_fragment::from_c_str(eq + 1));
×
865
                            }
866
                        }
867
                    } else {
11✔
868
                    }
869
                } else {
870
                    auto val_str = split_res.first.to_string();
16✔
871

872
                    if (seen_keys.count(val_str) == 0) {
16✔
873
                        seen_keys.insert(val_str);
16✔
874
                        query_map.gen(split_res.first);
16✔
875
                        query_map.gen();
16✔
876
                    }
877
                }
16✔
878

879
                if (split_res.second.empty()) {
27✔
880
                    break;
14✔
881
                }
882

883
                remaining = split_res.second;
13✔
884
            }
40✔
885
        } else {
14✔
886
            root.gen("query");
12✔
887
            root.gen();
12✔
888
            root.gen("parameters");
12✔
889
            root.gen();
12✔
890
        }
891
        root.gen("fragment");
26✔
892
        rc = curl_url_get(
26✔
893
            cu, CURLUPART_FRAGMENT, url_part.out(), CURLU_URLDECODE);
894
        if (rc == CURLUE_OK) {
26✔
895
            root.gen(string_fragment::from_c_str(url_part.in()));
3✔
896
        } else {
897
            root.gen();
23✔
898
        }
899
    }
26✔
900

901
    return json_string(gen);
26✔
902
}
27✔
903

904
struct url_parts {
905
    std::optional<std::string> up_scheme;
906
    std::optional<std::string> up_username;
907
    std::optional<std::string> up_password;
908
    std::optional<std::string> up_host;
909
    std::optional<std::string> up_port;
910
    std::optional<std::string> up_path;
911
    std::optional<std::string> up_query;
912
    std::map<std::string, std::optional<std::string>> up_parameters;
913
    std::optional<std::string> up_fragment;
914
};
915

916
const typed_json_path_container<url_parts>&
917
get_url_parts_handlers()
13✔
918
{
919
    static const json_path_container url_params_handlers = {
920
        yajlpp::pattern_property_handler("(?<param>.*)")
13✔
921
            .for_field(&url_parts::up_parameters),
13✔
922
    };
52✔
923

924
    static const typed_json_path_container<url_parts> retval = {
925
        yajlpp::property_handler("scheme").for_field(&url_parts::up_scheme),
26✔
926
        yajlpp::property_handler("username").for_field(&url_parts::up_username),
26✔
927
        yajlpp::property_handler("password").for_field(&url_parts::up_password),
26✔
928
        yajlpp::property_handler("host").for_field(&url_parts::up_host),
26✔
929
        yajlpp::property_handler("port").for_field(&url_parts::up_port),
26✔
930
        yajlpp::property_handler("path").for_field(&url_parts::up_path),
26✔
931
        yajlpp::property_handler("query").for_field(&url_parts::up_query),
26✔
932
        yajlpp::property_handler("parameters")
26✔
933
            .with_children(url_params_handlers),
13✔
934
        yajlpp::property_handler("fragment").for_field(&url_parts::up_fragment),
26✔
935
    };
156✔
936

937
    return retval;
13✔
938
}
156✔
939

940
auto_mem<char>
941
sql_unparse_url(string_fragment in)
13✔
942
{
943
    static auto* CURL_HANDLE = get_curl_easy();
13✔
944
    static const intern_string_t SRC = intern_string::lookup("arg");
39✔
945

946
    auto parse_res = get_url_parts_handlers().parser_for(SRC).of(in);
13✔
947
    if (parse_res.isErr()) {
13✔
948
        throw parse_res.unwrapErr()[0];
3✔
949
    }
950

951
    auto up = parse_res.unwrap();
10✔
952
    auto_mem<CURLU> cu(curl_url_cleanup);
10✔
953
    cu = curl_url();
10✔
954

955
    if (up.up_scheme) {
10✔
956
        curl_url_set(
9✔
957
            cu, CURLUPART_SCHEME, up.up_scheme->c_str(), CURLU_URLENCODE);
958
    }
959
    if (up.up_username) {
10✔
960
        curl_url_set(
×
961
            cu, CURLUPART_USER, up.up_username->c_str(), CURLU_URLENCODE);
962
    }
963
    if (up.up_password) {
10✔
964
        curl_url_set(
×
965
            cu, CURLUPART_PASSWORD, up.up_password->c_str(), CURLU_URLENCODE);
966
    }
967
    if (up.up_host) {
10✔
968
        curl_url_set(cu, CURLUPART_HOST, up.up_host->c_str(), CURLU_URLENCODE);
9✔
969
    }
970
    if (up.up_port) {
10✔
971
        curl_url_set(cu, CURLUPART_PORT, up.up_port->c_str(), 0);
×
972
    }
973
    if (up.up_path) {
10✔
974
        curl_url_set(cu, CURLUPART_PATH, up.up_path->c_str(), CURLU_URLENCODE);
4✔
975
    }
976
    if (up.up_query) {
10✔
977
        curl_url_set(cu, CURLUPART_QUERY, up.up_query->c_str(), 0);
4✔
978
    } else if (!up.up_parameters.empty()) {
6✔
979
        for (const auto& pair : up.up_parameters) {
×
980
            auto_mem<char> key(curl_free);
×
981
            auto_mem<char> value(curl_free);
×
982
            std::string qparam;
×
983

984
            key = curl_easy_escape(
985
                CURL_HANDLE, pair.first.c_str(), pair.first.length());
×
986
            if (pair.second) {
×
987
                value = curl_easy_escape(
988
                    CURL_HANDLE, pair.second->c_str(), pair.second->length());
×
989
                qparam = fmt::format(FMT_STRING("{}={}"), key.in(), value.in());
×
990
            } else {
991
                qparam = key.in();
×
992
            }
993

994
            curl_url_set(
×
995
                cu, CURLUPART_QUERY, qparam.c_str(), CURLU_APPENDQUERY);
996
        }
997
    }
998
    if (up.up_fragment) {
10✔
999
        curl_url_set(
1✔
1000
            cu, CURLUPART_FRAGMENT, up.up_fragment->c_str(), CURLU_URLENCODE);
1001
    }
1002

1003
    auto_mem<char> retval(curl_free);
10✔
1004

1005
    curl_url_get(cu, CURLUPART_URL, retval.out(), 0);
10✔
1006
    return retval;
20✔
1007
}
13✔
1008

1009
}  // namespace
1010

1011
json_string
1012
extract(const char* str)
16✔
1013
{
1014
    data_scanner ds(str);
16✔
1015
    data_parser dp(&ds);
16✔
1016

1017
    dp.parse();
16✔
1018
    // dp.print(stderr, dp.dp_pairs);
1019

1020
    yajlpp_gen gen;
16✔
1021
    yajl_gen_config(gen, yajl_gen_beautify, false);
16✔
1022

1023
    elements_to_json(gen, dp, &dp.dp_pairs);
16✔
1024

1025
    return json_string(gen);
32✔
1026
}
16✔
1027

1028
static std::string
1029
sql_humanize_id(string_fragment id)
7✔
1030
{
1031
    auto& vc = view_colors::singleton();
7✔
1032
    auto attrs = vc.attrs_for_ident(id.data(), id.length());
7✔
1033

1034
    return fmt::format(FMT_STRING("\x1b[38;5;{}m{}\x1b[0m"),
14✔
1035
                       // XXX attrs.ta_fg_color.value_or(COLOR_CYAN),
1036
                       (int8_t) ansi_color::cyan,
7✔
1037
                       id);
7✔
1038
}
1039

1040
static std::string
1041
sql_pretty_print(string_fragment in)
6✔
1042
{
1043
    data_scanner ds(in);
6✔
1044
    pretty_printer pp(&ds, {});
6✔
1045
    attr_line_t retval;
6✔
1046

1047
    pp.append_to(retval);
6✔
1048

1049
    return std::move(retval.get_string());
12✔
1050
}
6✔
1051

1052
int
1053
string_extension_functions(struct FuncDef** basic_funcs,
1,744✔
1054
                           struct FuncDefAgg** agg_funcs)
1055
{
1056
    static struct FuncDef string_funcs[] = {
1057
        sqlite_func_adapter<decltype(&regexp), regexp>::builder(
1058
            help_text("regexp", "Test if a string matches a regular expression")
×
1059
                .sql_function()
1,106✔
1060
                .with_parameter({"re", "The regular expression to use"})
2,212✔
1061
                .with_parameter({
2,212✔
1062
                    "str",
1063
                    "The string to test against the regular expression",
1064
                })),
1065

1066
        sqlite_func_adapter<decltype(&regexp_match), regexp_match>::builder(
2,212✔
1067
            help_text("regexp_match",
×
1068
                      "Match a string against a regular expression and return "
1069
                      "the capture groups as JSON.")
1070
                .sql_function()
1,106✔
1071
                .with_prql_path({"text", "regexp_match"})
1,106✔
1072
                .with_parameter({"re", "The regular expression to use"})
2,212✔
1073
                .with_parameter({
2,212✔
1074
                    "str",
1075
                    "The string to test against the regular expression",
1076
                })
1077
                .with_tags({"string", "regex"})
1,106✔
1078
                .with_example({
1,106✔
1079
                    "To capture the digits from the string '123'",
1080
                    "SELECT regexp_match('(\\d+)', '123')",
1081
                })
1082
                .with_example({
1,106✔
1083
                    "To capture a number and word into a JSON object with the "
1084
                    "properties 'col_0' and 'col_1'",
1085
                    "SELECT regexp_match('(\\d+) (\\w+)', '123 four')",
1086
                })
1087
                .with_example({
2,212✔
1088
                    "To capture a number and word into a JSON object with the "
1089
                    "named properties 'num' and 'str'",
1090
                    "SELECT regexp_match('(?<num>\\d+) (?<str>\\w+)', '123 "
1091
                    "four')",
1092
                }))
1093
            .with_result_subtype(),
1094

1095
        sqlite_func_adapter<decltype(&regexp_replace), regexp_replace>::builder(
1096
            help_text("regexp_replace",
×
1097
                      "Replace the parts of a string that match a regular "
1098
                      "expression.")
1099
                .sql_function()
1,106✔
1100
                .with_prql_path({"text", "regexp_replace"})
1,106✔
1101
                .with_parameter(
2,212✔
1102
                    {"str", "The string to perform replacements on"})
1103
                .with_parameter({"re", "The regular expression to match"})
2,212✔
1104
                .with_parameter({
2,212✔
1105
                    "repl",
1106
                    "The replacement string.  "
1107
                    "You can reference capture groups with a "
1108
                    "backslash followed by the number of the "
1109
                    "group, starting with 1.",
1110
                })
1111
                .with_tags({"string", "regex"})
1,106✔
1112
                .with_example({
1,106✔
1113
                    "To replace the word at the start of the string "
1114
                    "'Hello, World!' with 'Goodbye'",
1115
                    "SELECT regexp_replace('Hello, World!', "
1116
                    "'^(\\w+)', 'Goodbye')",
1117
                })
1118
                .with_example({
2,212✔
1119
                    "To wrap alphanumeric words with angle brackets",
1120
                    "SELECT regexp_replace('123 abc', '(\\w+)', '<\\1>')",
1121
                })),
1122

1123
        sqlite_func_adapter<decltype(&sql_humanize_file_size),
1124
                            sql_humanize_file_size>::
1125
            builder(help_text(
×
1126
                        "humanize_file_size",
1127
                        "Format the given file size as a human-friendly string")
1128
                        .sql_function()
1,106✔
1129
                        .with_prql_path({"humanize", "file_size"})
1,106✔
1130
                        .with_parameter({"value", "The file size to format"})
2,212✔
1131
                        .with_tags({"string"})
1,106✔
1132
                        .with_example({
2,212✔
1133
                            "To format an amount",
1134
                            "SELECT humanize_file_size(10 * 1024 * 1024)",
1135
                        })),
1136

1137
        sqlite_func_adapter<decltype(&sql_humanize_id), sql_humanize_id>::
1138
            builder(help_text("humanize_id",
×
1139
                              "Colorize the given ID using ANSI escape codes.")
1140
                        .sql_function()
1,106✔
1141
                        .with_prql_path({"humanize", "id"})
1,106✔
1142
                        .with_parameter({"id", "The identifier to color"})
2,212✔
1143
                        .with_tags({"string"})
1,106✔
1144
                        .with_example({
2,212✔
1145
                            "To colorize the ID 'cluster1'",
1146
                            "SELECT humanize_id('cluster1')",
1147
                        })),
1148

1149
        sqlite_func_adapter<decltype(&humanize::sparkline),
1150
                            humanize::sparkline>::
1151
            builder(
1152
                help_text("sparkline",
×
1153
                          "Function used to generate a sparkline bar chart.  "
1154
                          "The non-aggregate version converts a single numeric "
1155
                          "value on a range to a bar chart character.  The "
1156
                          "aggregate version returns a string with a bar "
1157
                          "character for every numeric input")
1158
                    .sql_function()
1,106✔
1159
                    .with_prql_path({"text", "sparkline"})
1,106✔
1160
                    .with_parameter({"value", "The numeric value to convert"})
2,212✔
1161
                    .with_parameter(help_text("upper",
3,318✔
1162
                                              "The upper bound of the numeric "
1163
                                              "range.  The non-aggregate "
1164
                                              "version defaults to 100.  The "
1165
                                              "aggregate version uses the "
1166
                                              "largest value in the inputs.")
1167
                                        .optional())
1,106✔
1168
                    .with_tags({"string"})
1,106✔
1169
                    .with_example({
1,106✔
1170
                        "To get the unicode block element for the "
1171
                        "value 32 in the "
1172
                        "range of 0-128",
1173
                        "SELECT sparkline(32, 128)",
1174
                    })
1175
                    .with_example({
2,212✔
1176
                        "To chart the values in a JSON array",
1177
                        "SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, "
1178
                        "4, 5, 6, 7, 8]')",
1179
                    })),
1180

1181
        sqlite_func_adapter<decltype(&sql_anonymize), sql_anonymize>::builder(
1182
            help_text("anonymize",
×
1183
                      "Replace identifying information with random values.")
1184
                .sql_function()
1,106✔
1185
                .with_prql_path({"text", "anonymize"})
1,106✔
1186
                .with_parameter({"value", "The text to anonymize"})
2,212✔
1187
                .with_tags({"string"})
1,106✔
1188
                .with_example({
2,212✔
1189
                    "To anonymize an IP address",
1190
                    "SELECT anonymize('Hello, 192.168.1.2')",
1191
                })),
1192

1193
        sqlite_func_adapter<decltype(&extract), extract>::builder(
2,212✔
1194
            help_text("extract",
×
1195
                      "Automatically Parse and extract data from a string")
1196
                .sql_function()
1,106✔
1197
                .with_prql_path({"text", "discover"})
1,106✔
1198
                .with_parameter({"str", "The string to parse"})
2,212✔
1199
                .with_tags({"string"})
1,106✔
1200
                .with_example({
1,106✔
1201
                    "To extract key/value pairs from a string",
1202
                    "SELECT extract('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1203
                })
1204
                .with_example({
2,212✔
1205
                    "To extract columnar data from a string",
1206
                    "SELECT extract('1.0 abc 2.0')",
1207
                }))
1208
            .with_result_subtype(),
1209

1210
        sqlite_func_adapter<decltype(&logfmt2json), logfmt2json>::builder(
2,212✔
1211
            help_text("logfmt2json",
1,106✔
1212
                      "Convert a logfmt-encoded string into JSON")
1213
                .sql_function()
1,106✔
1214
                .with_prql_path({"logfmt", "to_json"})
1,106✔
1215
                .with_parameter({"str", "The logfmt message to parse"})
2,212✔
1216
                .with_tags({"string"})
1,106✔
1217
                .with_example({
2,212✔
1218
                    "To extract key/value pairs from a log message",
1219
                    "SELECT logfmt2json('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1220
                }))
1221
            .with_result_subtype(),
1222

1223
        sqlite_func_adapter<
1224
            decltype(static_cast<bool (*)(const char*, const char*)>(
1225
                &startswith)),
1226
            startswith>::
1227
            builder(help_text("startswith",
×
1228
                              "Test if a string begins with the given prefix")
1229
                        .sql_function()
1,106✔
1230
                        .with_parameter({"str", "The string to test"})
2,212✔
1231
                        .with_parameter(
2,212✔
1232
                            {"prefix", "The prefix to check in the string"})
1233
                        .with_tags({"string"})
1,106✔
1234
                        .with_example({
1,106✔
1235
                            "To test if the string 'foobar' starts with 'foo'",
1236
                            "SELECT startswith('foobar', 'foo')",
1237
                        })
1238
                        .with_example({
2,212✔
1239
                            "To test if the string 'foobar' starts with 'bar'",
1240
                            "SELECT startswith('foobar', 'bar')",
1241
                        })),
1242

1243
        sqlite_func_adapter<decltype(static_cast<bool (*)(
1244
                                         const char*, const char*)>(&endswith)),
1245
                            endswith>::
1246
            builder(
1247
                help_text("endswith",
×
1248
                          "Test if a string ends with the given suffix")
1249
                    .sql_function()
1,106✔
1250
                    .with_parameter({"str", "The string to test"})
2,212✔
1251
                    .with_parameter(
2,212✔
1252
                        {"suffix", "The suffix to check in the string"})
1253
                    .with_tags({"string"})
1,106✔
1254
                    .with_example({
1,106✔
1255
                        "To test if the string 'notbad.jpg' ends with '.jpg'",
1256
                        "SELECT endswith('notbad.jpg', '.jpg')",
1257
                    })
1258
                    .with_example({
2,212✔
1259
                        "To test if the string 'notbad.png' starts with '.jpg'",
1260
                        "SELECT endswith('notbad.png', '.jpg')",
1261
                    })),
1262

1263
        sqlite_func_adapter<decltype(&sql_fuzzy_match), sql_fuzzy_match>::
1264
            builder(help_text(
×
1265
                        "fuzzy_match",
1266
                        "Perform a fuzzy match of a pattern against a "
1267
                        "string and return a score or NULL if the pattern was "
1268
                        "not matched")
1269
                        .sql_function()
1,106✔
1270
                        .with_parameter(help_text(
2,212✔
1271
                            "pattern", "The pattern to look for in the string"))
1272
                        .with_parameter(
1,106✔
1273
                            help_text("str", "The string to match against"))
2,212✔
1274
                        .with_tags({"string"})
1,106✔
1275
                        .with_example({
2,212✔
1276
                            "To match the pattern 'fo' against 'filter-out'",
1277
                            "SELECT fuzzy_match('fo', 'filter-out')",
1278
                        })),
1279

1280
        sqlite_func_adapter<decltype(&spooky_hash), spooky_hash>::builder(
1281
            help_text("spooky_hash",
×
1282
                      "Compute the hash value for the given arguments.")
1283
                .sql_function()
1,106✔
1284
                .with_parameter(
1,106✔
1285
                    help_text("str", "The string to hash").one_or_more())
2,212✔
1286
                .with_tags({"string"})
1,106✔
1287
                .with_example({
1,106✔
1288
                    "To produce a hash for the string 'Hello, World!'",
1289
                    "SELECT spooky_hash('Hello, World!')",
1290
                })
1291
                .with_example({
1,106✔
1292
                    "To produce a hash for the parameters where one is NULL",
1293
                    "SELECT spooky_hash('Hello, World!', NULL)",
1294
                })
1295
                .with_example({
1,106✔
1296
                    "To produce a hash for the parameters where one "
1297
                    "is an empty string",
1298
                    "SELECT spooky_hash('Hello, World!', '')",
1299
                })
1300
                .with_example({
2,212✔
1301
                    "To produce a hash for the parameters where one "
1302
                    "is a number",
1303
                    "SELECT spooky_hash('Hello, World!', 123)",
1304
                })),
1305

1306
        sqlite_func_adapter<decltype(&sql_gunzip), sql_gunzip>::builder(
1307
            help_text("gunzip", "Decompress a gzip file")
×
1308
                .sql_function()
1,106✔
1309
                .with_parameter(
1,106✔
1310
                    help_text("b", "The blob to decompress").one_or_more())
2,212✔
1311
                .with_tags({"string"})),
2,212✔
1312

1313
        sqlite_func_adapter<decltype(&sql_gzip), sql_gzip>::builder(
1314
            help_text("gzip", "Compress a string into a gzip file")
×
1315
                .sql_function()
1,106✔
1316
                .with_parameter(
1,106✔
1317
                    help_text("value", "The value to compress").one_or_more())
2,212✔
1318
                .with_tags({"string"})),
2,212✔
1319

1320
        sqlite_func_adapter<decltype(&sql_encode), sql_encode>::builder(
1321
            help_text("encode", "Encode the value using the given algorithm")
×
1322
                .sql_function()
1,106✔
1323
                .with_parameter(help_text("value", "The value to encode"))
2,212✔
1324
                .with_parameter(help_text("algorithm",
2,212✔
1325
                                          "One of the following encoding "
1326
                                          "algorithms: base64, hex, uri, html"))
1327
                .with_tags({"string"})
1,106✔
1328
                .with_example({
1,106✔
1329
                    "To base64-encode 'Hello, World!'",
1330
                    "SELECT encode('Hello, World!', 'base64')",
1331
                })
1332
                .with_example({
1,106✔
1333
                    "To hex-encode 'Hello, World!'",
1334
                    "SELECT encode('Hello, World!', 'hex')",
1335
                })
1336
                .with_example({
2,212✔
1337
                    "To URI-encode 'Hello, World!'",
1338
                    "SELECT encode('Hello, World!', 'uri')",
1339
                })),
1340

1341
        sqlite_func_adapter<decltype(&sql_decode), sql_decode>::builder(
1342
            help_text("decode", "Decode the value using the given algorithm")
×
1343
                .sql_function()
1,106✔
1344
                .with_parameter(help_text("value", "The value to decode"))
2,212✔
1345
                .with_parameter(help_text("algorithm",
2,212✔
1346
                                          "One of the following encoding "
1347
                                          "algorithms: base64, hex, uri"))
1348
                .with_tags({"string"})
1,106✔
1349
                .with_example({
2,212✔
1350
                    "To decode the URI-encoded string '%63%75%72%6c'",
1351
                    "SELECT decode('%63%75%72%6c', 'uri')",
1352
                })),
1353

1354
        sqlite_func_adapter<decltype(&sql_parse_url), sql_parse_url>::builder(
2,212✔
1355
            help_text("parse_url",
×
1356
                      "Parse a URL and return the components in a JSON object. "
1357
                      "Limitations: not all URL schemes are supported and "
1358
                      "repeated query parameters are not captured.")
1359
                .sql_function()
1,106✔
1360
                .with_parameter(help_text("url", "The URL to parse"))
2,212✔
1361
                .with_result({
2,212✔
1362
                    "scheme",
1363
                    "The URL's scheme",
1364
                })
1365
                .with_result({
2,212✔
1366
                    "username",
1367
                    "The name of the user specified in the URL",
1368
                })
1369
                .with_result({
2,212✔
1370
                    "password",
1371
                    "The password specified in the URL",
1372
                })
1373
                .with_result({
2,212✔
1374
                    "host",
1375
                    "The host name / IP specified in the URL",
1376
                })
1377
                .with_result({
2,212✔
1378
                    "port",
1379
                    "The port specified in the URL",
1380
                })
1381
                .with_result({
2,212✔
1382
                    "path",
1383
                    "The path specified in the URL",
1384
                })
1385
                .with_result({
2,212✔
1386
                    "query",
1387
                    "The query string in the URL",
1388
                })
1389
                .with_result({
2,212✔
1390
                    "parameters",
1391
                    "An object containing the query parameters",
1392
                })
1393
                .with_result({
2,212✔
1394
                    "fragment",
1395
                    "The fragment specified in the URL",
1396
                })
1397
                .with_tags({"string", "url"})
1,106✔
1398
                .with_example({
1,106✔
1399
                    "To parse the URL "
1400
                    "'https://example.com/search?q=hello%20world'",
1401
                    "SELECT "
1402
                    "parse_url('https://example.com/search?q=hello%20world')",
1403
                })
1404
                .with_example({
2,212✔
1405
                    "To parse the URL "
1406
                    "'https://alice@[fe80::14ff:4ee5:1215:2fb2]'",
1407
                    "SELECT "
1408
                    "parse_url('https://alice@[fe80::14ff:4ee5:1215:2fb2]')",
1409
                }))
1410
            .with_result_subtype(),
1411

1412
        sqlite_func_adapter<decltype(&sql_unparse_url), sql_unparse_url>::
1413
            builder(
1414
                help_text("unparse_url",
×
1415
                          "Convert a JSON object containing the parts of a "
1416
                          "URL into a URL string")
1417
                    .sql_function()
1,106✔
1418
                    .with_parameter(help_text(
2,212✔
1419
                        "obj", "The JSON object containing the URL parts"))
1420
                    .with_tags({"string", "url"})
1,106✔
1421
                    .with_example({
2,212✔
1422
                        "To unparse the object "
1423
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1424
                        "SELECT "
1425
                        "unparse_url('{\"scheme\": \"https\", \"host\": "
1426
                        "\"example.com\"}')",
1427
                    })),
1428

1429
        sqlite_func_adapter<decltype(&sql_pretty_print), sql_pretty_print>::
1430
            builder(
1431
                help_text("pretty_print", "Pretty-print the given string")
×
1432
                    .sql_function()
1,106✔
1433
                    .with_prql_path({"text", "pretty"})
1,106✔
1434
                    .with_parameter(help_text("str", "The string to format"))
2,212✔
1435
                    .with_tags({"string"})
1,106✔
1436
                    .with_example({
2,212✔
1437
                        "To pretty-print the string "
1438
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1439
                        "SELECT "
1440
                        "pretty_print('{\"scheme\": \"https\", \"host\": "
1441
                        "\"example.com\"}')",
1442
                    })),
1443

1444
        {nullptr},
1445
    };
44,878✔
1446

1447
    static struct FuncDefAgg str_agg_funcs[] = {
1448
        {
1449
            "group_spooky_hash",
1450
            -1,
1451
            SQLITE_UTF8,
1452
            0,
1453
            sql_spooky_hash_step,
1454
            sql_spooky_hash_final,
1455
            help_text("group_spooky_hash",
1,106✔
1456
                      "Compute the hash value for the given arguments")
1457
                .sql_agg_function()
1,106✔
1458
                .with_parameter(
1,106✔
1459
                    help_text("str", "The string to hash").one_or_more())
2,212✔
1460
                .with_tags({"string"})
1,106✔
1461
                .with_example({
2,212✔
1462
                    "To produce a hash of all of the values of 'column1'",
1463
                    "SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), "
1464
                    "('123'))",
1465
                }),
1466
        },
1467

1468
        {
1469
            "sparkline",
1470
            -1,
1471
            SQLITE_UTF8,
1472
            0,
1473
            sparkline_step,
1474
            sparkline_final,
1475
        },
1476

1477
        {nullptr},
1478
    };
2,850✔
1479

1480
    *basic_funcs = string_funcs;
1,744✔
1481
    *agg_funcs = str_agg_funcs;
1,744✔
1482

1483
    return SQLITE_OK;
1,744✔
1484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc