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

tstack / lnav / 18577582398-2579

16 Oct 2025 11:24PM UTC coverage: 69.212% (-0.9%) from 70.063%
18577582398-2579

push

github

tstack
[build] fix type

1 of 1 new or added line in 1 file covered. (100.0%)

2573 existing lines in 65 files now uncovered.

50113 of 72405 relevant lines covered (69.21%)

415651.95 hits per line

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

86.66
/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,791✔
91
{
92
    using re_cache_t
93
        = std::unordered_map<string_fragment, cache_entry, frag_hasher>;
94
    thread_local re_cache_t cache;
2,791✔
95

96
    auto iter = cache.find(re);
2,791✔
97
    if (iter == cache.end()) {
2,791✔
98
        auto compile_res = lnav::pcre2pp::code::from(re);
2,296✔
99
        if (compile_res.isErr()) {
2,296✔
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,295✔
106

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

111
        for (size_t lpc = 0; lpc < c.re2->get_capture_count(); lpc++) {
2,351✔
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,295✔
117
    }
2,296✔
118

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

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

127
    return reobj->re2->find_in(str).ignore_error().has_value();
2,248✔
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✔
UNCOV
148
        auto err = match_res.get<lnav::pcre2pp::matcher::error>();
×
149

UNCOV
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✔
UNCOV
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✔
UNCOV
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
    logfmt::parser p(line);
6✔
219
    yajlpp_gen gen;
6✔
220
    yajl_gen_config(gen, yajl_gen_beautify, false);
6✔
221

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✔
231
                [&root, &gen](const logfmt::parser::kvpair& kvp) {
25✔
232
                    root.gen(kvp.first);
19✔
233

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

249
                            jo.jo_ptr_callbacks = json_op::gen_callbacks;
5✔
250
                            jo.jo_ptr_data = gen;
5✔
251
                            parse_handle.reset(yajl_alloc(
5✔
252
                                &json_op::ptr_callbacks, nullptr, &jo));
253

254
                            const auto* json_in
255
                                = (const unsigned char*) qv.qv_value.data();
5✔
256
                            auto json_len = qv.qv_value.length();
5✔
257

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

270
                    return false;
19✔
271
                },
UNCOV
272
                [](const logfmt::parser::error& e) -> bool {
×
UNCOV
273
                    throw sqlite_func_error("Invalid logfmt: {}", e.e_msg);
×
274
                });
275
        }
25✔
276
    }
6✔
277

278
    return json_string(gen);
12✔
279
}
6✔
280

281
std::string
282
regexp_replace(string_fragment str, string_fragment re, const char* repl)
26✔
283
{
284
    auto* reobj = find_re(re);
26✔
285

286
    return reobj->re2->replace(str, repl);
26✔
287
}
288

289
std::optional<int64_t>
290
sql_fuzzy_match(const char* pat, const char* str)
5✔
291
{
292
    if (pat == nullptr) {
5✔
UNCOV
293
        return std::nullopt;
×
294
    }
295

296
    if (pat[0] == '\0') {
5✔
UNCOV
297
        return 1;
×
298
    }
299

300
    int score = 0;
5✔
301

302
    if (!fts::fuzzy_match(pat, str, score)) {
5✔
UNCOV
303
        return std::nullopt;
×
304
    }
305

306
    return score;
5✔
307
}
308

309
string_fragment
310
spooky_hash(const std::vector<const char*>& args)
5,527✔
311
{
312
    thread_local char hash_str_buf[hasher::STRING_SIZE];
313

314
    hasher context;
5,527✔
315
    for (const auto* const arg : args) {
16,576✔
316
        int64_t len = arg != nullptr ? strlen(arg) : 0;
11,049✔
317

318
        context.update((const char*) &len, sizeof(len));
11,049✔
319
        if (arg == nullptr) {
11,049✔
320
            continue;
30✔
321
        }
322
        context.update(arg, len);
11,019✔
323
    }
324
    context.to_string(hash_str_buf);
5,527✔
325

326
    return string_fragment::from_bytes(hash_str_buf, sizeof(hash_str_buf) - 1);
11,054✔
327
}
328

329
void
330
sql_spooky_hash_step(sqlite3_context* context, int argc, sqlite3_value** argv)
10✔
331
{
332
    auto* hasher
333
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
10✔
334

335
    for (int lpc = 0; lpc < argc; lpc++) {
20✔
336
        const auto* value = sqlite3_value_text(argv[lpc]);
10✔
337
        int64_t len = value != nullptr ? strlen((const char*) value) : 0;
10✔
338

339
        hasher->Update(&len, sizeof(len));
10✔
340
        if (value == nullptr) {
10✔
UNCOV
341
            continue;
×
342
        }
343
        hasher->Update(value, len);
10✔
344
    }
345
}
10✔
346

347
void
348
sql_spooky_hash_final(sqlite3_context* context)
5✔
349
{
350
    auto* hasher
351
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
5✔
352

353
    if (hasher == nullptr) {
5✔
UNCOV
354
        sqlite3_result_null(context);
×
355
    } else {
356
        byte_array<2, uint64> hash;
5✔
357

358
        hasher->Final(hash.out(0), hash.out(1));
5✔
359

360
        auto hex = hash.to_string();
5✔
361
        sqlite3_result_text(
5✔
362
            context, hex.c_str(), hex.length(), SQLITE_TRANSIENT);
5✔
363
    }
5✔
364
}
5✔
365

366
struct sparkline_context {
367
    bool sc_initialized{true};
368
    double sc_max_value{0.0};
369
    std::vector<double> sc_values;
370
};
371

372
void
373
sparkline_step(sqlite3_context* context, int argc, sqlite3_value** argv)
50✔
374
{
375
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
50✔
376
        context, sizeof(sparkline_context));
377

378
    if (!sc->sc_initialized) {
50✔
379
        new (sc) sparkline_context;
10✔
380
    }
381

382
    if (argc == 0) {
50✔
UNCOV
383
        return;
×
384
    }
385

386
    sc->sc_values.push_back(sqlite3_value_double(argv[0]));
50✔
387
    sc->sc_max_value = std::max(sc->sc_max_value, sc->sc_values.back());
50✔
388

389
    if (argc >= 2) {
50✔
390
        sc->sc_max_value
391
            = std::max(sc->sc_max_value, sqlite3_value_double(argv[1]));
5✔
392
    }
393
}
394

395
void
396
sparkline_final(sqlite3_context* context)
10✔
397
{
398
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
10✔
399
        context, sizeof(sparkline_context));
400

401
    if (!sc->sc_initialized) {
10✔
UNCOV
402
        sqlite3_result_text(context, "", 0, SQLITE_STATIC);
×
UNCOV
403
        return;
×
404
    }
405

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

409
    for (const auto& value : sc->sc_values) {
60✔
410
        auto bar = humanize::sparkline(value, sc->sc_max_value);
50✔
411

412
        strcpy(start, bar.c_str());
50✔
413
        start += bar.length();
50✔
414
    }
50✔
415
    *start = '\0';
10✔
416

417
    to_sqlite(context, std::move(retval));
10✔
418

419
    sc->~sparkline_context();
10✔
420
}
10✔
421

422
std::optional<mapbox::util::variant<blob_auto_buffer, sqlite3_int64, double>>
423
sql_gunzip(sqlite3_value* val)
2✔
424
{
425
    switch (sqlite3_value_type(val)) {
2✔
426
        case SQLITE3_TEXT:
2✔
427
        case SQLITE_BLOB: {
428
            const auto* buffer = sqlite3_value_blob(val);
2✔
429
            auto len = sqlite3_value_bytes(val);
2✔
430

431
            if (!lnav::gzip::is_gzipped((const char*) buffer, len)) {
2✔
UNCOV
432
                return blob_auto_buffer{
×
UNCOV
433
                    auto_buffer::from((const char*) buffer, len)};
×
434
            }
435

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

438
            if (res.isErr()) {
2✔
439
                throw sqlite_func_error("unable to uncompress -- {}",
440
                                        res.unwrapErr());
×
441
            }
442

443
            return blob_auto_buffer{res.unwrap()};
2✔
444
        }
2✔
UNCOV
445
        case SQLITE_INTEGER:
×
446
            return sqlite3_value_int64(val);
×
UNCOV
447
        case SQLITE_FLOAT:
×
UNCOV
448
            return sqlite3_value_double(val);
×
449
    }
450

UNCOV
451
    return std::nullopt;
×
452
}
453

454
std::optional<blob_auto_buffer>
455
sql_gzip(sqlite3_value* val)
3✔
456
{
457
    switch (sqlite3_value_type(val)) {
3✔
458
        case SQLITE3_TEXT:
1✔
459
        case SQLITE_BLOB: {
460
            const auto* buffer = sqlite3_value_blob(val);
1✔
461
            auto len = sqlite3_value_bytes(val);
1✔
462
            auto res = lnav::gzip::compress(buffer, len);
1✔
463

464
            if (res.isErr()) {
1✔
465
                throw sqlite_func_error("unable to compress -- {}",
UNCOV
466
                                        res.unwrapErr());
×
467
            }
468

469
            return blob_auto_buffer{res.unwrap()};
1✔
470
        }
1✔
471
        case SQLITE_INTEGER:
2✔
472
        case SQLITE_FLOAT: {
473
            const auto* buffer = sqlite3_value_text(val);
2✔
474
            auto res
475
                = lnav::gzip::compress(buffer, strlen((const char*) buffer));
2✔
476

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

482
            return blob_auto_buffer{res.unwrap()};
2✔
483
        }
2✔
484
    }
485

UNCOV
486
    return std::nullopt;
×
487
}
488

489
#if defined(HAVE_LIBCURL)
490
static CURL*
491
get_curl_easy()
45✔
492
{
493
    static struct curl_wrapper {
494
        curl_wrapper() { this->cw_value = curl_easy_init(); }
26✔
495

496
        auto_mem<CURL> cw_value{curl_easy_cleanup};
497
    } retval;
45✔
498

499
    return retval.cw_value.in();
45✔
500
}
501
#endif
502

503
mapbox::util::variant<text_auto_buffer, auto_mem<char>, null_value_t>
504
sql_encode(sqlite3_value* value, encode_algo algo)
20✔
505
{
506
    switch (sqlite3_value_type(value)) {
20✔
507
        case SQLITE_NULL: {
1✔
508
            return null_value_t{};
1✔
509
        }
510
        case SQLITE_BLOB: {
1✔
511
            const auto* blob
512
                = static_cast<const char*>(sqlite3_value_blob(value));
1✔
513
            auto blob_len = sqlite3_value_bytes(value);
1✔
514

515
            switch (algo) {
1✔
516
                case encode_algo::base64: {
1✔
517
                    auto buf = auto_buffer::alloc((blob_len * 5) / 3);
1✔
518
                    auto outlen = buf.capacity();
1✔
519

520
                    base64_encode(blob, blob_len, buf.in(), &outlen, 0);
1✔
521
                    buf.resize(outlen);
1✔
522
                    return text_auto_buffer{std::move(buf)};
1✔
523
                }
1✔
524
                case encode_algo::hex: {
×
525
                    auto buf = auto_buffer::alloc(blob_len * 2 + 1);
×
526

UNCOV
527
                    for (int lpc = 0; lpc < blob_len; lpc++) {
×
528
                        fmt::format_to(std::back_inserter(buf),
×
UNCOV
529
                                       FMT_STRING("{:02x}"),
×
UNCOV
530
                                       blob[lpc]);
×
531
                    }
532

UNCOV
533
                    return text_auto_buffer{std::move(buf)};
×
534
                }
535
#if defined(HAVE_LIBCURL)
UNCOV
536
                case encode_algo::uri: {
×
UNCOV
537
                    auto_mem<char> retval(curl_free);
×
538

UNCOV
539
                    retval = curl_easy_escape(get_curl_easy(), blob, blob_len);
×
UNCOV
540
                    return std::move(retval);
×
541
                }
542
#endif
UNCOV
543
                case encode_algo::html: {
×
UNCOV
544
                    return md4cpp::escape_html(
×
UNCOV
545
                        string_fragment::from_bytes(blob, blob_len));
×
546
                }
547
            }
548
        }
549
        default: {
550
            const auto* text = (const char*) sqlite3_value_text(value);
18✔
551
            auto text_len = sqlite3_value_bytes(value);
18✔
552

553
            switch (algo) {
18✔
554
                case encode_algo::base64: {
5✔
555
                    auto buf = auto_buffer::alloc((text_len * 5) / 3);
5✔
556
                    size_t outlen = buf.capacity();
5✔
557

558
                    base64_encode(text, text_len, buf.in(), &outlen, 0);
5✔
559
                    buf.resize(outlen);
5✔
560
                    return text_auto_buffer{std::move(buf)};
5✔
561
                }
5✔
562
                case encode_algo::hex: {
7✔
563
                    auto buf = auto_buffer::alloc(text_len * 2 + 1);
7✔
564

565
                    for (int lpc = 0; lpc < text_len; lpc++) {
88✔
566
                        fmt::format_to(std::back_inserter(buf),
81✔
567
                                       FMT_STRING("{:02x}"),
243✔
568
                                       text[lpc]);
81✔
569
                    }
570

571
                    return text_auto_buffer{std::move(buf)};
7✔
572
                }
7✔
573
#if defined(HAVE_LIBCURL)
574
                case encode_algo::uri: {
5✔
575
                    auto_mem<char> retval(curl_free);
5✔
576

577
                    retval = curl_easy_escape(get_curl_easy(), text, text_len);
5✔
578
                    return std::move(retval);
5✔
579
                }
5✔
580
#endif
581
                case encode_algo::html: {
1✔
582
                    return md4cpp::escape_html(
2✔
583
                        string_fragment::from_bytes(text, text_len));
1✔
584
                }
585
            }
586
        }
587
    }
UNCOV
588
    ensure(false);
×
589
}
590

591
mapbox::util::variant<blob_auto_buffer, text_auto_buffer, auto_mem<char>>
592
sql_decode(string_fragment str, encode_algo algo)
10✔
593
{
594
    switch (algo) {
10✔
595
        case encode_algo::base64: {
1✔
596
            auto buf = auto_buffer::alloc(str.length());
1✔
597
            auto outlen = buf.capacity();
1✔
598
            base64_decode(str.data(), str.length(), buf.in(), &outlen, 0);
1✔
599
            buf.resize(outlen);
1✔
600

601
            return blob_auto_buffer{std::move(buf)};
1✔
602
        }
1✔
603
        case encode_algo::hex: {
3✔
604
            auto buf = auto_buffer::alloc(str.length() / 2);
3✔
605
            auto sv = str.to_string_view();
3✔
606

607
            if (sv.size() % 2 != 0) {
3✔
608
                throw sqlite_func_error(
609
                    "hex input is not a multiple of two characters");
2✔
610
            }
611
            while (sv.size() >= 2) {
16✔
612
                auto scan_res = scn::scan<uint8_t>(sv.substr(0, 2), "{:2x}");
15✔
613
                if (!scan_res) {
15✔
614
                    throw sqlite_func_error(
615
                        "invalid hex input at: {}",
616
                        std::distance(str.begin(), sv.begin()));
3✔
617
                }
618
                auto value = scan_res->value();
14✔
619
                buf.push_back((char) (value & 0xff));
14✔
620
                sv = sv.substr(2);
14✔
621
            }
622

623
            return blob_auto_buffer{std::move(buf)};
1✔
624
        }
3✔
625
#if defined(HAVE_LIBCURL)
626
        case encode_algo::uri: {
5✔
627
            auto_mem<char> retval(curl_free);
5✔
628

629
            retval = curl_easy_unescape(
630
                get_curl_easy(), str.data(), str.length(), nullptr);
5✔
631

632
            return std::move(retval);
5✔
633
        }
5✔
634
#endif
635
        case encode_algo::html: {
1✔
636
            static const auto ENTITY_RE = lnav::pcre2pp::code::from_const(
637
                R"(&(?:([a-z0-9]+)|#([0-9]{1,6})|#x([0-9a-fA-F]{1,6}));)",
638
                PCRE2_CASELESS);
1✔
639
            static const auto ENTITIES = md4cpp::get_xml_entity_map();
1✔
640

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

693
std::string
694
sql_humanize_file_size(file_ssize_t value)
279✔
695
{
696
    return humanize::file_size(value, humanize::alignment::columnar);
279✔
697
}
698

699
std::string
700
sql_anonymize(string_fragment frag)
202✔
701
{
702
    static safe::Safe<lnav::text_anonymizer> ta;
202✔
703

704
    return ta.writeAccess()->next(frag);
202✔
705
}
706

707
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
708
extern "C"
709
{
710
const char* curl_url_strerror(CURLUcode error);
711
}
712
#endif
713

714
json_string
715
sql_parse_url(std::string url)
27✔
716
{
717
    static auto* CURL_HANDLE = get_curl_easy();
27✔
718

719
    auto_mem<CURLU> cu(curl_url_cleanup);
27✔
720
    cu = curl_url();
27✔
721

722
    auto rc = curl_url_set(
27✔
723
        cu, CURLUPART_URL, url.c_str(), CURLU_NON_SUPPORT_SCHEME);
724
    if (rc != CURLUE_OK) {
27✔
725
        auto_mem<char> url_part(curl_free);
1✔
726
        yajlpp_gen gen;
1✔
727
        yajl_gen_config(gen, yajl_gen_beautify, false);
1✔
728

729
        {
730
            yajlpp_map root(gen);
1✔
731
            root.gen("error");
1✔
732
            root.gen("invalid-url");
1✔
733
            root.gen("url");
1✔
734
            root.gen(url);
1✔
735
            root.gen("reason");
1✔
736
            root.gen(curl_url_strerror(rc));
1✔
737
        }
1✔
738

739
        return json_string(gen);
1✔
740
    }
1✔
741

742
    auto_mem<char> url_part(curl_free);
26✔
743
    yajlpp_gen gen;
26✔
744
    yajl_gen_config(gen, yajl_gen_beautify, false);
26✔
745

746
    {
747
        yajlpp_map root(gen);
26✔
748

749
        root.gen("scheme");
26✔
750
        rc = curl_url_get(cu, CURLUPART_SCHEME, url_part.out(), 0);
26✔
751
        if (rc == CURLUE_OK) {
26✔
752
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
753
        } else {
UNCOV
754
            root.gen();
×
755
        }
756
        root.gen("username");
26✔
757
        rc = curl_url_get(cu, CURLUPART_USER, url_part.out(), CURLU_URLDECODE);
26✔
758
        if (rc == CURLUE_OK) {
26✔
759
            root.gen(string_fragment::from_c_str(url_part.in()));
5✔
760
        } else {
761
            root.gen();
21✔
762
        }
763
        root.gen("password");
26✔
764
        rc = curl_url_get(
26✔
765
            cu, CURLUPART_PASSWORD, url_part.out(), CURLU_URLDECODE);
766
        if (rc == CURLUE_OK) {
26✔
UNCOV
767
            root.gen(string_fragment::from_c_str(url_part.in()));
×
768
        } else {
769
            root.gen();
26✔
770
        }
771
        root.gen("host");
26✔
772
        rc = curl_url_get(cu, CURLUPART_HOST, url_part.out(), CURLU_URLDECODE);
26✔
773
        if (rc == CURLUE_OK) {
26✔
774
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
775
        } else {
UNCOV
776
            root.gen();
×
777
        }
778
        root.gen("port");
26✔
779
        rc = curl_url_get(cu, CURLUPART_PORT, url_part.out(), 0);
26✔
780
        if (rc == CURLUE_OK) {
26✔
UNCOV
781
            root.gen(string_fragment::from_c_str(url_part.in()));
×
782
        } else {
783
            root.gen();
26✔
784
        }
785
        root.gen("path");
26✔
786
        rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE);
26✔
787
        if (rc == CURLUE_OK) {
26✔
788
            auto path_frag = string_fragment::from_c_str(url_part.in());
26✔
789
            auto path_utf_res = is_utf8(path_frag);
26✔
790
            if (path_utf_res.is_valid()) {
26✔
791
                root.gen(path_frag);
26✔
792
            } else {
UNCOV
793
                rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), 0);
×
UNCOV
794
                if (rc == CURLUE_OK) {
×
UNCOV
795
                    root.gen(string_fragment::from_c_str(url_part.in()));
×
796
                } else {
UNCOV
797
                    root.gen();
×
798
                }
799
            }
800
        } else {
UNCOV
801
            root.gen();
×
802
        }
803
        rc = curl_url_get(cu, CURLUPART_QUERY, url_part.out(), 0);
26✔
804
        if (rc == CURLUE_OK) {
26✔
805
            root.gen("query");
14✔
806
            root.gen(string_fragment::from_c_str(url_part.in()));
14✔
807

808
            root.gen("parameters");
14✔
809
            robin_hood::unordered_set<std::string> seen_keys;
14✔
810
            yajlpp_map query_map(gen);
14✔
811

812
            for (size_t lpc = 0; url_part.in()[lpc]; lpc++) {
206✔
813
                if (url_part.in()[lpc] == '+') {
192✔
814
                    url_part.in()[lpc] = ' ';
1✔
815
                }
816
            }
817
            auto query_frag = string_fragment::from_c_str(url_part.in());
14✔
818
            auto remaining = query_frag;
14✔
819

820
            while (true) {
821
                auto split_res
822
                    = remaining.split_when(string_fragment::tag1{'&'});
27✔
823
                auto_mem<char> kv_pair(curl_free);
27✔
824
                auto kv_pair_encoded = split_res.first;
27✔
825
                int out_len = 0;
27✔
826

827
                kv_pair = curl_easy_unescape(CURL_HANDLE,
828
                                             kv_pair_encoded.data(),
829
                                             kv_pair_encoded.length(),
830
                                             &out_len);
27✔
831

832
                auto kv_pair_frag
833
                    = string_fragment::from_bytes(kv_pair.in(), out_len);
27✔
834
                auto eq_index_opt = kv_pair_frag.find('=');
27✔
835
                if (eq_index_opt) {
27✔
836
                    auto key = kv_pair_frag.sub_range(0, eq_index_opt.value());
11✔
837
                    auto val = kv_pair_frag.substr(eq_index_opt.value() + 1);
11✔
838

839
                    auto key_utf_res = is_utf8(key);
11✔
840
                    auto val_utf_res = is_utf8(val);
11✔
841
                    if (key_utf_res.is_valid()) {
11✔
842
                        auto key_str = key.to_string();
11✔
843

844
                        if (seen_keys.count(key_str) == 0) {
11✔
845
                            seen_keys.emplace(key_str);
11✔
846
                            query_map.gen(key);
11✔
847
                            if (val_utf_res.is_valid()) {
11✔
848
                                query_map.gen(val);
11✔
849
                            } else {
UNCOV
850
                                auto eq = strchr(kv_pair_encoded.data(), '=');
×
UNCOV
851
                                query_map.gen(
×
UNCOV
852
                                    string_fragment::from_c_str(eq + 1));
×
853
                            }
854
                        }
855
                    } else {
11✔
856
                    }
857
                } else {
858
                    auto val_str = split_res.first.to_string();
16✔
859

860
                    if (seen_keys.count(val_str) == 0) {
16✔
861
                        seen_keys.insert(val_str);
16✔
862
                        query_map.gen(split_res.first);
16✔
863
                        query_map.gen();
16✔
864
                    }
865
                }
16✔
866

867
                if (split_res.second.empty()) {
27✔
868
                    break;
14✔
869
                }
870

871
                remaining = split_res.second;
13✔
872
            }
40✔
873
        } else {
14✔
874
            root.gen("query");
12✔
875
            root.gen();
12✔
876
            root.gen("parameters");
12✔
877
            root.gen();
12✔
878
        }
879
        root.gen("fragment");
26✔
880
        rc = curl_url_get(
26✔
881
            cu, CURLUPART_FRAGMENT, url_part.out(), CURLU_URLDECODE);
882
        if (rc == CURLUE_OK) {
26✔
883
            root.gen(string_fragment::from_c_str(url_part.in()));
3✔
884
        } else {
885
            root.gen();
23✔
886
        }
887
    }
26✔
888

889
    return json_string(gen);
26✔
890
}
27✔
891

892
struct url_parts {
893
    std::optional<std::string> up_scheme;
894
    std::optional<std::string> up_username;
895
    std::optional<std::string> up_password;
896
    std::optional<std::string> up_host;
897
    std::optional<std::string> up_port;
898
    std::optional<std::string> up_path;
899
    std::optional<std::string> up_query;
900
    std::map<std::string, std::optional<std::string>> up_parameters;
901
    std::optional<std::string> up_fragment;
902
};
903

904
const typed_json_path_container<url_parts>&
905
get_url_parts_handlers()
13✔
906
{
907
    static const json_path_container url_params_handlers = {
908
        yajlpp::pattern_property_handler("(?<param>.*)")
13✔
909
            .for_field(&url_parts::up_parameters),
13✔
910
    };
52✔
911

912
    static const typed_json_path_container<url_parts> retval = {
913
        yajlpp::property_handler("scheme").for_field(&url_parts::up_scheme),
26✔
914
        yajlpp::property_handler("username").for_field(&url_parts::up_username),
26✔
915
        yajlpp::property_handler("password").for_field(&url_parts::up_password),
26✔
916
        yajlpp::property_handler("host").for_field(&url_parts::up_host),
26✔
917
        yajlpp::property_handler("port").for_field(&url_parts::up_port),
26✔
918
        yajlpp::property_handler("path").for_field(&url_parts::up_path),
26✔
919
        yajlpp::property_handler("query").for_field(&url_parts::up_query),
26✔
920
        yajlpp::property_handler("parameters")
26✔
921
            .with_children(url_params_handlers),
13✔
922
        yajlpp::property_handler("fragment").for_field(&url_parts::up_fragment),
26✔
923
    };
156✔
924

925
    return retval;
13✔
926
}
156✔
927

928
auto_mem<char>
929
sql_unparse_url(string_fragment in)
13✔
930
{
931
    static auto* CURL_HANDLE = get_curl_easy();
13✔
932
    static const intern_string_t SRC = intern_string::lookup("arg");
39✔
933

934
    auto parse_res = get_url_parts_handlers().parser_for(SRC).of(in);
13✔
935
    if (parse_res.isErr()) {
13✔
936
        throw parse_res.unwrapErr()[0];
3✔
937
    }
938

939
    auto up = parse_res.unwrap();
10✔
940
    auto_mem<CURLU> cu(curl_url_cleanup);
10✔
941
    cu = curl_url();
10✔
942

943
    if (up.up_scheme) {
10✔
944
        curl_url_set(
9✔
945
            cu, CURLUPART_SCHEME, up.up_scheme->c_str(), CURLU_URLENCODE);
946
    }
947
    if (up.up_username) {
10✔
UNCOV
948
        curl_url_set(
×
949
            cu, CURLUPART_USER, up.up_username->c_str(), CURLU_URLENCODE);
950
    }
951
    if (up.up_password) {
10✔
UNCOV
952
        curl_url_set(
×
953
            cu, CURLUPART_PASSWORD, up.up_password->c_str(), CURLU_URLENCODE);
954
    }
955
    if (up.up_host) {
10✔
956
        curl_url_set(cu, CURLUPART_HOST, up.up_host->c_str(), CURLU_URLENCODE);
9✔
957
    }
958
    if (up.up_port) {
10✔
UNCOV
959
        curl_url_set(cu, CURLUPART_PORT, up.up_port->c_str(), 0);
×
960
    }
961
    if (up.up_path) {
10✔
962
        curl_url_set(cu, CURLUPART_PATH, up.up_path->c_str(), CURLU_URLENCODE);
4✔
963
    }
964
    if (up.up_query) {
10✔
965
        curl_url_set(cu, CURLUPART_QUERY, up.up_query->c_str(), 0);
4✔
966
    } else if (!up.up_parameters.empty()) {
6✔
UNCOV
967
        for (const auto& pair : up.up_parameters) {
×
UNCOV
968
            auto_mem<char> key(curl_free);
×
UNCOV
969
            auto_mem<char> value(curl_free);
×
UNCOV
970
            std::string qparam;
×
971

972
            key = curl_easy_escape(
UNCOV
973
                CURL_HANDLE, pair.first.c_str(), pair.first.length());
×
UNCOV
974
            if (pair.second) {
×
975
                value = curl_easy_escape(
UNCOV
976
                    CURL_HANDLE, pair.second->c_str(), pair.second->length());
×
UNCOV
977
                qparam = fmt::format(FMT_STRING("{}={}"), key.in(), value.in());
×
978
            } else {
979
                qparam = key.in();
×
980
            }
981

UNCOV
982
            curl_url_set(
×
983
                cu, CURLUPART_QUERY, qparam.c_str(), CURLU_APPENDQUERY);
984
        }
985
    }
986
    if (up.up_fragment) {
10✔
987
        curl_url_set(
1✔
988
            cu, CURLUPART_FRAGMENT, up.up_fragment->c_str(), CURLU_URLENCODE);
989
    }
990

991
    auto_mem<char> retval(curl_free);
10✔
992

993
    curl_url_get(cu, CURLUPART_URL, retval.out(), 0);
10✔
994
    return retval;
20✔
995
}
13✔
996

997
}  // namespace
998

999
json_string
1000
extract(const char* str)
16✔
1001
{
1002
    data_scanner ds(str);
16✔
1003
    data_parser dp(&ds);
16✔
1004

1005
    dp.parse();
16✔
1006
    // dp.print(stderr, dp.dp_pairs);
1007

1008
    yajlpp_gen gen;
16✔
1009
    yajl_gen_config(gen, yajl_gen_beautify, false);
16✔
1010

1011
    elements_to_json(gen, dp, &dp.dp_pairs);
16✔
1012

1013
    return json_string(gen);
32✔
1014
}
16✔
1015

1016
static std::string
1017
sql_humanize_id(string_fragment id)
7✔
1018
{
1019
    auto& vc = view_colors::singleton();
7✔
1020
    auto attrs = vc.attrs_for_ident(id.data(), id.length());
7✔
1021

1022
    return fmt::format(FMT_STRING("\x1b[38;5;{}m{}\x1b[0m"),
14✔
1023
                       // XXX attrs.ta_fg_color.value_or(COLOR_CYAN),
1024
                       (int8_t) ansi_color::cyan,
7✔
1025
                       id);
14✔
1026
}
7✔
1027

1028
static std::string
1029
sql_pretty_print(string_fragment in)
6✔
1030
{
1031
    data_scanner ds(in);
6✔
1032
    pretty_printer pp(&ds, {});
6✔
1033
    attr_line_t retval;
6✔
1034

1035
    pp.append_to(retval);
6✔
1036

1037
    return std::move(retval.get_string());
12✔
1038
}
6✔
1039

1040
int
1041
string_extension_functions(struct FuncDef** basic_funcs,
1,672✔
1042
                           struct FuncDefAgg** agg_funcs)
1043
{
1044
    static struct FuncDef string_funcs[] = {
1045
        sqlite_func_adapter<decltype(&regexp), regexp>::builder(
1046
            help_text("regexp", "Test if a string matches a regular expression")
×
1047
                .sql_function()
1,068✔
1048
                .with_parameter({"re", "The regular expression to use"})
2,136✔
1049
                .with_parameter({
2,136✔
1050
                    "str",
1051
                    "The string to test against the regular expression",
1052
                })),
1053

1054
        sqlite_func_adapter<decltype(&regexp_match), regexp_match>::builder(
2,136✔
UNCOV
1055
            help_text("regexp_match",
×
1056
                      "Match a string against a regular expression and return "
1057
                      "the capture groups as JSON.")
1058
                .sql_function()
1,068✔
1059
                .with_prql_path({"text", "regexp_match"})
1,068✔
1060
                .with_parameter({"re", "The regular expression to use"})
2,136✔
1061
                .with_parameter({
2,136✔
1062
                    "str",
1063
                    "The string to test against the regular expression",
1064
                })
1065
                .with_tags({"string", "regex"})
1,068✔
1066
                .with_example({
1,068✔
1067
                    "To capture the digits from the string '123'",
1068
                    "SELECT regexp_match('(\\d+)', '123')",
1069
                })
1070
                .with_example({
1,068✔
1071
                    "To capture a number and word into a JSON object with the "
1072
                    "properties 'col_0' and 'col_1'",
1073
                    "SELECT regexp_match('(\\d+) (\\w+)', '123 four')",
1074
                })
1075
                .with_example({
2,136✔
1076
                    "To capture a number and word into a JSON object with the "
1077
                    "named properties 'num' and 'str'",
1078
                    "SELECT regexp_match('(?<num>\\d+) (?<str>\\w+)', '123 "
1079
                    "four')",
1080
                }))
1081
            .with_result_subtype(),
1082

1083
        sqlite_func_adapter<decltype(&regexp_replace), regexp_replace>::builder(
UNCOV
1084
            help_text("regexp_replace",
×
1085
                      "Replace the parts of a string that match a regular "
1086
                      "expression.")
1087
                .sql_function()
1,068✔
1088
                .with_prql_path({"text", "regexp_replace"})
1,068✔
1089
                .with_parameter(
2,136✔
1090
                    {"str", "The string to perform replacements on"})
1091
                .with_parameter({"re", "The regular expression to match"})
2,136✔
1092
                .with_parameter({
2,136✔
1093
                    "repl",
1094
                    "The replacement string.  "
1095
                    "You can reference capture groups with a "
1096
                    "backslash followed by the number of the "
1097
                    "group, starting with 1.",
1098
                })
1099
                .with_tags({"string", "regex"})
1,068✔
1100
                .with_example({
1,068✔
1101
                    "To replace the word at the start of the string "
1102
                    "'Hello, World!' with 'Goodbye'",
1103
                    "SELECT regexp_replace('Hello, World!', "
1104
                    "'^(\\w+)', 'Goodbye')",
1105
                })
1106
                .with_example({
2,136✔
1107
                    "To wrap alphanumeric words with angle brackets",
1108
                    "SELECT regexp_replace('123 abc', '(\\w+)', '<\\1>')",
1109
                })),
1110

1111
        sqlite_func_adapter<decltype(&sql_humanize_file_size),
1112
                            sql_humanize_file_size>::
UNCOV
1113
            builder(help_text(
×
1114
                        "humanize_file_size",
1115
                        "Format the given file size as a human-friendly string")
1116
                        .sql_function()
1,068✔
1117
                        .with_prql_path({"humanize", "file_size"})
1,068✔
1118
                        .with_parameter({"value", "The file size to format"})
2,136✔
1119
                        .with_tags({"string"})
1,068✔
1120
                        .with_example({
2,136✔
1121
                            "To format an amount",
1122
                            "SELECT humanize_file_size(10 * 1024 * 1024)",
1123
                        })),
1124

1125
        sqlite_func_adapter<decltype(&sql_humanize_id), sql_humanize_id>::
UNCOV
1126
            builder(help_text("humanize_id",
×
1127
                              "Colorize the given ID using ANSI escape codes.")
1128
                        .sql_function()
1,068✔
1129
                        .with_prql_path({"humanize", "id"})
1,068✔
1130
                        .with_parameter({"id", "The identifier to color"})
2,136✔
1131
                        .with_tags({"string"})
1,068✔
1132
                        .with_example({
2,136✔
1133
                            "To colorize the ID 'cluster1'",
1134
                            "SELECT humanize_id('cluster1')",
1135
                        })),
1136

1137
        sqlite_func_adapter<decltype(&humanize::sparkline),
1138
                            humanize::sparkline>::
1139
            builder(
UNCOV
1140
                help_text("sparkline",
×
1141
                          "Function used to generate a sparkline bar chart.  "
1142
                          "The non-aggregate version converts a single numeric "
1143
                          "value on a range to a bar chart character.  The "
1144
                          "aggregate version returns a string with a bar "
1145
                          "character for every numeric input")
1146
                    .sql_function()
1,068✔
1147
                    .with_prql_path({"text", "sparkline"})
1,068✔
1148
                    .with_parameter({"value", "The numeric value to convert"})
2,136✔
1149
                    .with_parameter(help_text("upper",
3,204✔
1150
                                              "The upper bound of the numeric "
1151
                                              "range.  The non-aggregate "
1152
                                              "version defaults to 100.  The "
1153
                                              "aggregate version uses the "
1154
                                              "largest value in the inputs.")
1155
                                        .optional())
1,068✔
1156
                    .with_tags({"string"})
1,068✔
1157
                    .with_example({
1,068✔
1158
                        "To get the unicode block element for the "
1159
                        "value 32 in the "
1160
                        "range of 0-128",
1161
                        "SELECT sparkline(32, 128)",
1162
                    })
1163
                    .with_example({
2,136✔
1164
                        "To chart the values in a JSON array",
1165
                        "SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, "
1166
                        "4, 5, 6, 7, 8]')",
1167
                    })),
1168

1169
        sqlite_func_adapter<decltype(&sql_anonymize), sql_anonymize>::builder(
UNCOV
1170
            help_text("anonymize",
×
1171
                      "Replace identifying information with random values.")
1172
                .sql_function()
1,068✔
1173
                .with_prql_path({"text", "anonymize"})
1,068✔
1174
                .with_parameter({"value", "The text to anonymize"})
2,136✔
1175
                .with_tags({"string"})
1,068✔
1176
                .with_example({
2,136✔
1177
                    "To anonymize an IP address",
1178
                    "SELECT anonymize('Hello, 192.168.1.2')",
1179
                })),
1180

1181
        sqlite_func_adapter<decltype(&extract), extract>::builder(
2,136✔
UNCOV
1182
            help_text("extract",
×
1183
                      "Automatically Parse and extract data from a string")
1184
                .sql_function()
1,068✔
1185
                .with_prql_path({"text", "discover"})
1,068✔
1186
                .with_parameter({"str", "The string to parse"})
2,136✔
1187
                .with_tags({"string"})
1,068✔
1188
                .with_example({
1,068✔
1189
                    "To extract key/value pairs from a string",
1190
                    "SELECT extract('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1191
                })
1192
                .with_example({
2,136✔
1193
                    "To extract columnar data from a string",
1194
                    "SELECT extract('1.0 abc 2.0')",
1195
                }))
1196
            .with_result_subtype(),
1197

1198
        sqlite_func_adapter<decltype(&logfmt2json), logfmt2json>::builder(
2,136✔
1199
            help_text("logfmt2json",
1,068✔
1200
                      "Convert a logfmt-encoded string into JSON")
1201
                .sql_function()
1,068✔
1202
                .with_prql_path({"logfmt", "to_json"})
1,068✔
1203
                .with_parameter({"str", "The logfmt message to parse"})
2,136✔
1204
                .with_tags({"string"})
1,068✔
1205
                .with_example({
2,136✔
1206
                    "To extract key/value pairs from a log message",
1207
                    "SELECT logfmt2json('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1208
                }))
1209
            .with_result_subtype(),
1210

1211
        sqlite_func_adapter<
1212
            decltype(static_cast<bool (*)(const char*, const char*)>(
1213
                &startswith)),
1214
            startswith>::
UNCOV
1215
            builder(help_text("startswith",
×
1216
                              "Test if a string begins with the given prefix")
1217
                        .sql_function()
1,068✔
1218
                        .with_parameter({"str", "The string to test"})
2,136✔
1219
                        .with_parameter(
2,136✔
1220
                            {"prefix", "The prefix to check in the string"})
1221
                        .with_tags({"string"})
1,068✔
1222
                        .with_example({
1,068✔
1223
                            "To test if the string 'foobar' starts with 'foo'",
1224
                            "SELECT startswith('foobar', 'foo')",
1225
                        })
1226
                        .with_example({
2,136✔
1227
                            "To test if the string 'foobar' starts with 'bar'",
1228
                            "SELECT startswith('foobar', 'bar')",
1229
                        })),
1230

1231
        sqlite_func_adapter<decltype(static_cast<bool (*)(
1232
                                         const char*, const char*)>(&endswith)),
1233
                            endswith>::
1234
            builder(
1235
                help_text("endswith",
×
1236
                          "Test if a string ends with the given suffix")
1237
                    .sql_function()
1,068✔
1238
                    .with_parameter({"str", "The string to test"})
2,136✔
1239
                    .with_parameter(
2,136✔
1240
                        {"suffix", "The suffix to check in the string"})
1241
                    .with_tags({"string"})
1,068✔
1242
                    .with_example({
1,068✔
1243
                        "To test if the string 'notbad.jpg' ends with '.jpg'",
1244
                        "SELECT endswith('notbad.jpg', '.jpg')",
1245
                    })
1246
                    .with_example({
2,136✔
1247
                        "To test if the string 'notbad.png' starts with '.jpg'",
1248
                        "SELECT endswith('notbad.png', '.jpg')",
1249
                    })),
1250

1251
        sqlite_func_adapter<decltype(&sql_fuzzy_match), sql_fuzzy_match>::
UNCOV
1252
            builder(help_text(
×
1253
                        "fuzzy_match",
1254
                        "Perform a fuzzy match of a pattern against a "
1255
                        "string and return a score or NULL if the pattern was "
1256
                        "not matched")
1257
                        .sql_function()
1,068✔
1258
                        .with_parameter(help_text(
2,136✔
1259
                            "pattern", "The pattern to look for in the string"))
1260
                        .with_parameter(
1,068✔
1261
                            help_text("str", "The string to match against"))
2,136✔
1262
                        .with_tags({"string"})
1,068✔
1263
                        .with_example({
2,136✔
1264
                            "To match the pattern 'fo' against 'filter-out'",
1265
                            "SELECT fuzzy_match('fo', 'filter-out')",
1266
                        })),
1267

1268
        sqlite_func_adapter<decltype(&spooky_hash), spooky_hash>::builder(
UNCOV
1269
            help_text("spooky_hash",
×
1270
                      "Compute the hash value for the given arguments.")
1271
                .sql_function()
1,068✔
1272
                .with_parameter(
1,068✔
1273
                    help_text("str", "The string to hash").one_or_more())
2,136✔
1274
                .with_tags({"string"})
1,068✔
1275
                .with_example({
1,068✔
1276
                    "To produce a hash for the string 'Hello, World!'",
1277
                    "SELECT spooky_hash('Hello, World!')",
1278
                })
1279
                .with_example({
1,068✔
1280
                    "To produce a hash for the parameters where one is NULL",
1281
                    "SELECT spooky_hash('Hello, World!', NULL)",
1282
                })
1283
                .with_example({
1,068✔
1284
                    "To produce a hash for the parameters where one "
1285
                    "is an empty string",
1286
                    "SELECT spooky_hash('Hello, World!', '')",
1287
                })
1288
                .with_example({
2,136✔
1289
                    "To produce a hash for the parameters where one "
1290
                    "is a number",
1291
                    "SELECT spooky_hash('Hello, World!', 123)",
1292
                })),
1293

1294
        sqlite_func_adapter<decltype(&sql_gunzip), sql_gunzip>::builder(
UNCOV
1295
            help_text("gunzip", "Decompress a gzip file")
×
1296
                .sql_function()
1,068✔
1297
                .with_parameter(
1,068✔
1298
                    help_text("b", "The blob to decompress").one_or_more())
2,136✔
1299
                .with_tags({"string"})),
2,136✔
1300

1301
        sqlite_func_adapter<decltype(&sql_gzip), sql_gzip>::builder(
UNCOV
1302
            help_text("gzip", "Compress a string into a gzip file")
×
1303
                .sql_function()
1,068✔
1304
                .with_parameter(
1,068✔
1305
                    help_text("value", "The value to compress").one_or_more())
2,136✔
1306
                .with_tags({"string"})),
2,136✔
1307

1308
        sqlite_func_adapter<decltype(&sql_encode), sql_encode>::builder(
UNCOV
1309
            help_text("encode", "Encode the value using the given algorithm")
×
1310
                .sql_function()
1,068✔
1311
                .with_parameter(help_text("value", "The value to encode"))
2,136✔
1312
                .with_parameter(help_text("algorithm",
2,136✔
1313
                                          "One of the following encoding "
1314
                                          "algorithms: base64, hex, uri, html"))
1315
                .with_tags({"string"})
1,068✔
1316
                .with_example({
1,068✔
1317
                    "To base64-encode 'Hello, World!'",
1318
                    "SELECT encode('Hello, World!', 'base64')",
1319
                })
1320
                .with_example({
1,068✔
1321
                    "To hex-encode 'Hello, World!'",
1322
                    "SELECT encode('Hello, World!', 'hex')",
1323
                })
1324
                .with_example({
2,136✔
1325
                    "To URI-encode 'Hello, World!'",
1326
                    "SELECT encode('Hello, World!', 'uri')",
1327
                })),
1328

1329
        sqlite_func_adapter<decltype(&sql_decode), sql_decode>::builder(
UNCOV
1330
            help_text("decode", "Decode the value using the given algorithm")
×
1331
                .sql_function()
1,068✔
1332
                .with_parameter(help_text("value", "The value to decode"))
2,136✔
1333
                .with_parameter(help_text("algorithm",
2,136✔
1334
                                          "One of the following encoding "
1335
                                          "algorithms: base64, hex, uri"))
1336
                .with_tags({"string"})
1,068✔
1337
                .with_example({
2,136✔
1338
                    "To decode the URI-encoded string '%63%75%72%6c'",
1339
                    "SELECT decode('%63%75%72%6c', 'uri')",
1340
                })),
1341

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

1400
        sqlite_func_adapter<decltype(&sql_unparse_url), sql_unparse_url>::
1401
            builder(
UNCOV
1402
                help_text("unparse_url",
×
1403
                          "Convert a JSON object containing the parts of a "
1404
                          "URL into a URL string")
1405
                    .sql_function()
1,068✔
1406
                    .with_parameter(help_text(
2,136✔
1407
                        "obj", "The JSON object containing the URL parts"))
1408
                    .with_tags({"string", "url"})
1,068✔
1409
                    .with_example({
2,136✔
1410
                        "To unparse the object "
1411
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1412
                        "SELECT "
1413
                        "unparse_url('{\"scheme\": \"https\", \"host\": "
1414
                        "\"example.com\"}')",
1415
                    })),
1416

1417
        sqlite_func_adapter<decltype(&sql_pretty_print), sql_pretty_print>::
1418
            builder(
UNCOV
1419
                help_text("pretty_print", "Pretty-print the given string")
×
1420
                    .sql_function()
1,068✔
1421
                    .with_prql_path({"text", "pretty"})
1,068✔
1422
                    .with_parameter(help_text("str", "The string to format"))
2,136✔
1423
                    .with_tags({"string"})
1,068✔
1424
                    .with_example({
2,136✔
1425
                        "To pretty-print the string "
1426
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1427
                        "SELECT "
1428
                        "pretty_print('{\"scheme\": \"https\", \"host\": "
1429
                        "\"example.com\"}')",
1430
                    })),
1431

1432
        {nullptr},
1433
    };
43,324✔
1434

1435
    static struct FuncDefAgg str_agg_funcs[] = {
1436
        {
1437
            "group_spooky_hash",
1438
            -1,
1439
            SQLITE_UTF8,
1440
            0,
1441
            sql_spooky_hash_step,
1442
            sql_spooky_hash_final,
1443
            help_text("group_spooky_hash",
1,068✔
1444
                      "Compute the hash value for the given arguments")
1445
                .sql_agg_function()
1,068✔
1446
                .with_parameter(
1,068✔
1447
                    help_text("str", "The string to hash").one_or_more())
2,136✔
1448
                .with_tags({"string"})
1,068✔
1449
                .with_example({
2,136✔
1450
                    "To produce a hash of all of the values of 'column1'",
1451
                    "SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), "
1452
                    "('123'))",
1453
                }),
1454
        },
1455

1456
        {
1457
            "sparkline",
1458
            -1,
1459
            SQLITE_UTF8,
1460
            0,
1461
            sparkline_step,
1462
            sparkline_final,
1463
        },
1464

1465
        {nullptr},
1466
    };
2,740✔
1467

1468
    *basic_funcs = string_funcs;
1,672✔
1469
    *agg_funcs = str_agg_funcs;
1,672✔
1470

1471
    return SQLITE_OK;
1,672✔
1472
}
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