• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

tstack / lnav / 17589970077-2502

09 Sep 2025 05:00PM UTC coverage: 65.196% (-5.0%) from 70.225%
17589970077-2502

push

github

tstack
[format] add fields for source file/line

Knowing the source file/line context in a log
message can help find log messages when using
log2src.

56 of 70 new or added lines in 2 files covered. (80.0%)

13954 existing lines in 210 files now uncovered.

45516 of 69814 relevant lines covered (65.2%)

404154.37 hits per line

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

84.79
/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 "pcrepp/pcre2pp.hh"
32
#include "pretty_printer.hh"
33
#include "safe/safe.h"
34
#include "scn/scan.h"
35
#include "sqlite-extension-func.hh"
36
#include "text_anonymizer.hh"
37
#include "view_curses.hh"
38
#include "vtab_module.hh"
39
#include "vtab_module_json.hh"
40
#include "yajl/api/yajl_gen.h"
41
#include "yajlpp/json_op.hh"
42
#include "yajlpp/yajlpp.hh"
43
#include "yajlpp/yajlpp_def.hh"
44

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

49
enum class encode_algo {
50
    base64,
51
    hex,
52
    uri,
53
};
54

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

61
        if (strcasecmp(algo_name, "base64") == 0) {
25✔
62
            return encode_algo::base64;
8✔
63
        }
64
        if (strcasecmp(algo_name, "hex") == 0) {
17✔
65
            return encode_algo::hex;
6✔
66
        }
67
        if (strcasecmp(algo_name, "uri") == 0) {
11✔
68
            return encode_algo::uri;
10✔
69
        }
70

71
        throw from_sqlite_conversion_error("value of 'base64', 'hex', or 'uri'",
72
                                           argi);
1✔
73
    }
74
};
75

76
namespace {
77

78
struct cache_entry {
79
    std::shared_ptr<lnav::pcre2pp::code> re2;
80
    std::shared_ptr<column_namer> cn{
81
        std::make_shared<column_namer>(column_namer::language::JSON)};
82
};
83

84
cache_entry*
85
find_re(string_fragment re)
2,737✔
86
{
87
    using re_cache_t
88
        = std::unordered_map<string_fragment, cache_entry, frag_hasher>;
89
    thread_local re_cache_t cache;
2,737✔
90

91
    auto iter = cache.find(re);
2,737✔
92
    if (iter == cache.end()) {
2,737✔
93
        auto compile_res = lnav::pcre2pp::code::from(re);
2,242✔
94
        if (compile_res.isErr()) {
2,242✔
95
            const static intern_string_t SRC = intern_string::lookup("arg");
3✔
96

97
            throw lnav::console::to_user_message(SRC, compile_res.unwrapErr());
1✔
98
        }
99

100
        cache_entry c;
2,241✔
101

102
        c.re2 = compile_res.unwrap().to_shared();
2,241✔
103
        auto pair = cache.insert(
2,241✔
104
            std::make_pair(string_fragment::from_str(c.re2->get_pattern()), c));
4,482✔
105

106
        for (size_t lpc = 0; lpc < c.re2->get_capture_count(); lpc++) {
2,297✔
107
            c.cn->add_column(string_fragment::from_c_str(
56✔
108
                c.re2->get_name_for_capture(lpc + 1)));
109
        }
110

111
        iter = pair.first;
2,241✔
112
    }
2,242✔
113

114
    return &iter->second;
5,472✔
115
}
116

117
bool
118
regexp(string_fragment re, string_fragment str)
2,194✔
119
{
120
    auto* reobj = find_re(re);
2,194✔
121

122
    return reobj->re2->find_in(str).ignore_error().has_value();
2,194✔
123
}
124

125
mapbox::util::
126
    variant<int64_t, double, const char*, string_fragment, json_string>
127
    regexp_match(string_fragment re, string_fragment str)
517✔
128
{
129
    auto* reobj = find_re(re);
517✔
130
    auto& extractor = *reobj->re2;
516✔
131

132
    if (extractor.get_capture_count() == 0) {
516✔
133
        throw std::runtime_error(
134
            "regular expression does not have any captures");
1✔
135
    }
136

137
    auto md = extractor.create_match_data();
515✔
138
    auto match_res = extractor.capture_from(str).into(md).matches();
515✔
139
    if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
515✔
140
        return static_cast<const char*>(nullptr);
1✔
141
    }
142
    if (match_res.is<lnav::pcre2pp::matcher::error>()) {
514✔
UNCOV
143
        auto err = match_res.get<lnav::pcre2pp::matcher::error>();
×
144

UNCOV
145
        throw std::runtime_error(err.get_message());
×
146
    }
147

148
    if (extractor.get_capture_count() == 1) {
514✔
149
        auto cap = md[1];
500✔
150

151
        if (!cap) {
500✔
UNCOV
152
            return static_cast<const char*>(nullptr);
×
153
        }
154

155
        auto scan_int_res = scn::scan_int<int64_t>(cap->to_string_view());
500✔
156
        if (scan_int_res) {
500✔
157
            if (scan_int_res->range().empty()) {
15✔
158
                return scan_int_res->value();
15✔
159
            }
160
            auto scan_float_res
161
                = scn::scan_value<double>(cap->to_string_view());
1✔
162
            if (scan_float_res && scan_float_res->range().empty()) {
1✔
163
                return scan_float_res->value();
1✔
164
            }
165
        }
166

167
        return cap.value();
485✔
168
    }
169

170
    yajlpp_gen gen;
14✔
171
    yajl_gen_config(gen, yajl_gen_beautify, false);
14✔
172
    {
173
        yajlpp_map root_map(gen);
14✔
174

175
        for (size_t lpc = 0; lpc < extractor.get_capture_count(); lpc++) {
42✔
176
            const auto& colname = reobj->cn->cn_names[lpc];
28✔
177
            const auto cap = md[lpc + 1];
28✔
178

179
            yajl_gen_pstring(gen, colname.data(), colname.length());
28✔
180

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

201
    return json_string(gen);
14✔
202
#if 0
203
    sqlite3_result_text(ctx, (const char *) buf, len, SQLITE_TRANSIENT);
204
#    ifdef HAVE_SQLITE3_VALUE_SUBTYPE
205
    sqlite3_result_subtype(ctx, JSON_SUBTYPE);
206
#    endif
207
#endif
208
}
515✔
209

210
static json_string
211
logfmt2json(string_fragment line)
6✔
212
{
213
    logfmt::parser p(line);
6✔
214
    yajlpp_gen gen;
6✔
215
    yajl_gen_config(gen, yajl_gen_beautify, false);
6✔
216

217
    {
218
        yajlpp_map root(gen);
6✔
219
        bool done = false;
6✔
220

221
        while (!done) {
31✔
222
            auto pair = p.step();
25✔
223

224
            done = pair.match(
50✔
225
                [](const logfmt::parser::end_of_input& eoi) { return true; },
6✔
226
                [&root, &gen](const logfmt::parser::kvpair& kvp) {
25✔
227
                    root.gen(kvp.first);
19✔
228

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

244
                            jo.jo_ptr_callbacks = json_op::gen_callbacks;
5✔
245
                            jo.jo_ptr_data = gen;
5✔
246
                            parse_handle.reset(yajl_alloc(
5✔
247
                                &json_op::ptr_callbacks, nullptr, &jo));
248

249
                            const auto* json_in
250
                                = (const unsigned char*) qv.qv_value.data();
5✔
251
                            auto json_len = qv.qv_value.length();
5✔
252

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

265
                    return false;
19✔
266
                },
UNCOV
267
                [](const logfmt::parser::error& e) -> bool {
×
UNCOV
268
                    throw sqlite_func_error("Invalid logfmt: {}", e.e_msg);
×
269
                });
270
        }
25✔
271
    }
6✔
272

273
    return json_string(gen);
12✔
274
}
6✔
275

276
std::string
277
regexp_replace(string_fragment str, string_fragment re, const char* repl)
26✔
278
{
279
    auto* reobj = find_re(re);
26✔
280

281
    return reobj->re2->replace(str, repl);
26✔
282
}
283

284
std::optional<int64_t>
285
sql_fuzzy_match(const char* pat, const char* str)
5✔
286
{
287
    if (pat == nullptr) {
5✔
UNCOV
288
        return std::nullopt;
×
289
    }
290

291
    if (pat[0] == '\0') {
5✔
UNCOV
292
        return 1;
×
293
    }
294

295
    int score = 0;
5✔
296

297
    if (!fts::fuzzy_match(pat, str, score)) {
5✔
UNCOV
298
        return std::nullopt;
×
299
    }
300

301
    return score;
5✔
302
}
303

304
string_fragment
305
spooky_hash(const std::vector<const char*>& args)
5,527✔
306
{
307
    thread_local char hash_str_buf[hasher::STRING_SIZE];
308

309
    hasher context;
5,527✔
310
    for (const auto* const arg : args) {
16,576✔
311
        int64_t len = arg != nullptr ? strlen(arg) : 0;
11,049✔
312

313
        context.update((const char*) &len, sizeof(len));
11,049✔
314
        if (arg == nullptr) {
11,049✔
315
            continue;
30✔
316
        }
317
        context.update(arg, len);
11,019✔
318
    }
319
    context.to_string(hash_str_buf);
5,527✔
320

321
    return string_fragment::from_bytes(hash_str_buf, sizeof(hash_str_buf) - 1);
11,054✔
322
}
323

324
void
325
sql_spooky_hash_step(sqlite3_context* context, int argc, sqlite3_value** argv)
10✔
326
{
327
    auto* hasher
328
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
10✔
329

330
    for (int lpc = 0; lpc < argc; lpc++) {
20✔
331
        const auto* value = sqlite3_value_text(argv[lpc]);
10✔
332
        int64_t len = value != nullptr ? strlen((const char*) value) : 0;
10✔
333

334
        hasher->Update(&len, sizeof(len));
10✔
335
        if (value == nullptr) {
10✔
UNCOV
336
            continue;
×
337
        }
338
        hasher->Update(value, len);
10✔
339
    }
340
}
10✔
341

342
void
343
sql_spooky_hash_final(sqlite3_context* context)
5✔
344
{
345
    auto* hasher
346
        = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
5✔
347

348
    if (hasher == nullptr) {
5✔
UNCOV
349
        sqlite3_result_null(context);
×
350
    } else {
351
        byte_array<2, uint64> hash;
5✔
352

353
        hasher->Final(hash.out(0), hash.out(1));
5✔
354

355
        auto hex = hash.to_string();
5✔
356
        sqlite3_result_text(
5✔
357
            context, hex.c_str(), hex.length(), SQLITE_TRANSIENT);
5✔
358
    }
5✔
359
}
5✔
360

361
struct sparkline_context {
362
    bool sc_initialized{true};
363
    double sc_max_value{0.0};
364
    std::vector<double> sc_values;
365
};
366

367
void
368
sparkline_step(sqlite3_context* context, int argc, sqlite3_value** argv)
50✔
369
{
370
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
50✔
371
        context, sizeof(sparkline_context));
372

373
    if (!sc->sc_initialized) {
50✔
374
        new (sc) sparkline_context;
10✔
375
    }
376

377
    if (argc == 0) {
50✔
UNCOV
378
        return;
×
379
    }
380

381
    sc->sc_values.push_back(sqlite3_value_double(argv[0]));
50✔
382
    sc->sc_max_value = std::max(sc->sc_max_value, sc->sc_values.back());
50✔
383

384
    if (argc >= 2) {
50✔
385
        sc->sc_max_value
386
            = std::max(sc->sc_max_value, sqlite3_value_double(argv[1]));
5✔
387
    }
388
}
389

390
void
391
sparkline_final(sqlite3_context* context)
10✔
392
{
393
    auto* sc = (sparkline_context*) sqlite3_aggregate_context(
10✔
394
        context, sizeof(sparkline_context));
395

396
    if (!sc->sc_initialized) {
10✔
UNCOV
397
        sqlite3_result_text(context, "", 0, SQLITE_STATIC);
×
UNCOV
398
        return;
×
399
    }
400

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

404
    for (const auto& value : sc->sc_values) {
60✔
405
        auto bar = humanize::sparkline(value, sc->sc_max_value);
50✔
406

407
        strcpy(start, bar.c_str());
50✔
408
        start += bar.length();
50✔
409
    }
50✔
410
    *start = '\0';
10✔
411

412
    to_sqlite(context, std::move(retval));
10✔
413

414
    sc->~sparkline_context();
10✔
415
}
10✔
416

417
std::optional<mapbox::util::variant<blob_auto_buffer, sqlite3_int64, double>>
418
sql_gunzip(sqlite3_value* val)
2✔
419
{
420
    switch (sqlite3_value_type(val)) {
2✔
421
        case SQLITE3_TEXT:
2✔
422
        case SQLITE_BLOB: {
423
            const auto* buffer = sqlite3_value_blob(val);
2✔
424
            auto len = sqlite3_value_bytes(val);
2✔
425

426
            if (!lnav::gzip::is_gzipped((const char*) buffer, len)) {
2✔
UNCOV
427
                return blob_auto_buffer{
×
UNCOV
428
                    auto_buffer::from((const char*) buffer, len)};
×
429
            }
430

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

433
            if (res.isErr()) {
2✔
434
                throw sqlite_func_error("unable to uncompress -- {}",
UNCOV
435
                                        res.unwrapErr());
×
436
            }
437

438
            return blob_auto_buffer{res.unwrap()};
2✔
439
        }
2✔
440
        case SQLITE_INTEGER:
×
UNCOV
441
            return sqlite3_value_int64(val);
×
UNCOV
442
        case SQLITE_FLOAT:
×
UNCOV
443
            return sqlite3_value_double(val);
×
444
    }
445

UNCOV
446
    return std::nullopt;
×
447
}
448

449
std::optional<blob_auto_buffer>
450
sql_gzip(sqlite3_value* val)
3✔
451
{
452
    switch (sqlite3_value_type(val)) {
3✔
453
        case SQLITE3_TEXT:
1✔
454
        case SQLITE_BLOB: {
455
            const auto* buffer = sqlite3_value_blob(val);
1✔
456
            auto len = sqlite3_value_bytes(val);
1✔
457
            auto res = lnav::gzip::compress(buffer, len);
1✔
458

459
            if (res.isErr()) {
1✔
460
                throw sqlite_func_error("unable to compress -- {}",
UNCOV
461
                                        res.unwrapErr());
×
462
            }
463

464
            return blob_auto_buffer{res.unwrap()};
1✔
465
        }
1✔
466
        case SQLITE_INTEGER:
2✔
467
        case SQLITE_FLOAT: {
468
            const auto* buffer = sqlite3_value_text(val);
2✔
469
            auto res
470
                = lnav::gzip::compress(buffer, strlen((const char*) buffer));
2✔
471

472
            if (res.isErr()) {
2✔
473
                throw sqlite_func_error("unable to compress -- {}",
UNCOV
474
                                        res.unwrapErr());
×
475
            }
476

477
            return blob_auto_buffer{res.unwrap()};
2✔
478
        }
2✔
479
    }
480

UNCOV
481
    return std::nullopt;
×
482
}
483

484
#if defined(HAVE_LIBCURL)
485
static CURL*
486
get_curl_easy()
45✔
487
{
488
    static struct curl_wrapper {
489
        curl_wrapper() { this->cw_value = curl_easy_init(); }
26✔
490

491
        auto_mem<CURL> cw_value{curl_easy_cleanup};
492
    } retval;
45✔
493

494
    return retval.cw_value.in();
45✔
495
}
496
#endif
497

498
mapbox::util::variant<text_auto_buffer, auto_mem<char>, null_value_t>
499
sql_encode(sqlite3_value* value, encode_algo algo)
18✔
500
{
501
    switch (sqlite3_value_type(value)) {
18✔
502
        case SQLITE_NULL: {
1✔
503
            return null_value_t{};
1✔
504
        }
505
        case SQLITE_BLOB: {
1✔
506
            const auto* blob
507
                = static_cast<const char*>(sqlite3_value_blob(value));
1✔
508
            auto blob_len = sqlite3_value_bytes(value);
1✔
509

510
            switch (algo) {
1✔
511
                case encode_algo::base64: {
1✔
512
                    auto buf = auto_buffer::alloc((blob_len * 5) / 3);
1✔
513
                    auto outlen = buf.capacity();
1✔
514

515
                    base64_encode(blob, blob_len, buf.in(), &outlen, 0);
1✔
516
                    buf.resize(outlen);
1✔
517
                    return text_auto_buffer{std::move(buf)};
1✔
518
                }
1✔
UNCOV
519
                case encode_algo::hex: {
×
UNCOV
520
                    auto buf = auto_buffer::alloc(blob_len * 2 + 1);
×
521

UNCOV
522
                    for (int lpc = 0; lpc < blob_len; lpc++) {
×
UNCOV
523
                        fmt::format_to(std::back_inserter(buf),
×
UNCOV
524
                                       FMT_STRING("{:x}"),
×
UNCOV
525
                                       blob[lpc]);
×
526
                    }
527

UNCOV
528
                    return text_auto_buffer{std::move(buf)};
×
529
                }
530
#if defined(HAVE_LIBCURL)
UNCOV
531
                case encode_algo::uri: {
×
UNCOV
532
                    auto_mem<char> retval(curl_free);
×
533

UNCOV
534
                    retval = curl_easy_escape(get_curl_easy(), blob, blob_len);
×
UNCOV
535
                    return std::move(retval);
×
536
                }
537
#endif
538
            }
539
        }
540
        default: {
541
            const auto* text = (const char*) sqlite3_value_text(value);
16✔
542
            auto text_len = sqlite3_value_bytes(value);
16✔
543

544
            switch (algo) {
16✔
545
                case encode_algo::base64: {
5✔
546
                    auto buf = auto_buffer::alloc((text_len * 5) / 3);
5✔
547
                    size_t outlen = buf.capacity();
5✔
548

549
                    base64_encode(text, text_len, buf.in(), &outlen, 0);
5✔
550
                    buf.resize(outlen);
5✔
551
                    return text_auto_buffer{std::move(buf)};
5✔
552
                }
5✔
553
                case encode_algo::hex: {
6✔
554
                    auto buf = auto_buffer::alloc(text_len * 2 + 1);
6✔
555

556
                    for (int lpc = 0; lpc < text_len; lpc++) {
74✔
557
                        fmt::format_to(std::back_inserter(buf),
68✔
558
                                       FMT_STRING("{:02x}"),
204✔
559
                                       text[lpc]);
68✔
560
                    }
561

562
                    return text_auto_buffer{std::move(buf)};
6✔
563
                }
6✔
564
#if defined(HAVE_LIBCURL)
565
                case encode_algo::uri: {
5✔
566
                    auto_mem<char> retval(curl_free);
5✔
567

568
                    retval = curl_easy_escape(get_curl_easy(), text, text_len);
5✔
569
                    return std::move(retval);
5✔
570
                }
5✔
571
#endif
572
            }
573
        }
574
    }
575
    ensure(false);
×
576
}
577

578
mapbox::util::variant<blob_auto_buffer, auto_mem<char>>
579
sql_decode(string_fragment str, encode_algo algo)
6✔
580
{
581
    switch (algo) {
6✔
582
        case encode_algo::base64: {
1✔
583
            auto buf = auto_buffer::alloc(str.length());
1✔
584
            auto outlen = buf.capacity();
1✔
585
            base64_decode(str.data(), str.length(), buf.in(), &outlen, 0);
1✔
586
            buf.resize(outlen);
1✔
587

588
            return blob_auto_buffer{std::move(buf)};
1✔
589
        }
1✔
UNCOV
590
        case encode_algo::hex: {
×
UNCOV
591
            auto buf = auto_buffer::alloc(str.length() / 2);
×
UNCOV
592
            auto sv = str.to_string_view();
×
593

UNCOV
594
            while (!sv.empty()) {
×
UNCOV
595
                auto scan_res = scn::scan<int32_t>(sv, "{:2x}");
×
UNCOV
596
                if (!scan_res) {
×
597
                    throw sqlite_func_error(
598
                        "invalid hex input at: {}",
UNCOV
599
                        std::distance(str.begin(), sv.begin()));
×
600
                }
UNCOV
601
                auto value = scan_res->value();
×
602
                buf.push_back((char) (value & 0xff));
×
UNCOV
603
                sv = std::string_view{scan_res->range().begin(),
×
UNCOV
604
                                      scan_res->range().size()};
×
605
            }
606

UNCOV
607
            return blob_auto_buffer{std::move(buf)};
×
608
        }
609
#if defined(HAVE_LIBCURL)
610
        case encode_algo::uri: {
5✔
611
            auto_mem<char> retval(curl_free);
5✔
612

613
            retval = curl_easy_unescape(
614
                get_curl_easy(), str.data(), str.length(), nullptr);
5✔
615

616
            return std::move(retval);
5✔
617
        }
5✔
618
#endif
619
    }
UNCOV
620
    ensure(false);
×
621
}
622

623
std::string
624
sql_humanize_file_size(file_ssize_t value)
279✔
625
{
626
    return humanize::file_size(value, humanize::alignment::columnar);
279✔
627
}
628

629
std::string
630
sql_anonymize(string_fragment frag)
202✔
631
{
632
    static safe::Safe<lnav::text_anonymizer> ta;
202✔
633

634
    return ta.writeAccess()->next(frag);
202✔
635
}
636

637
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
638
extern "C"
639
{
640
const char* curl_url_strerror(CURLUcode error);
641
}
642
#endif
643

644
json_string
645
sql_parse_url(std::string url)
27✔
646
{
647
    static auto* CURL_HANDLE = get_curl_easy();
27✔
648

649
    auto_mem<CURLU> cu(curl_url_cleanup);
27✔
650
    cu = curl_url();
27✔
651

652
    auto rc = curl_url_set(
27✔
653
        cu, CURLUPART_URL, url.c_str(), CURLU_NON_SUPPORT_SCHEME);
654
    if (rc != CURLUE_OK) {
27✔
655
        auto_mem<char> url_part(curl_free);
1✔
656
        yajlpp_gen gen;
1✔
657
        yajl_gen_config(gen, yajl_gen_beautify, false);
1✔
658

659
        {
660
            yajlpp_map root(gen);
1✔
661
            root.gen("error");
1✔
662
            root.gen("invalid-url");
1✔
663
            root.gen("url");
1✔
664
            root.gen(url);
1✔
665
            root.gen("reason");
1✔
666
            root.gen(curl_url_strerror(rc));
1✔
667
        }
1✔
668

669
        return json_string(gen);
1✔
670
    }
1✔
671

672
    auto_mem<char> url_part(curl_free);
26✔
673
    yajlpp_gen gen;
26✔
674
    yajl_gen_config(gen, yajl_gen_beautify, false);
26✔
675

676
    {
677
        yajlpp_map root(gen);
26✔
678

679
        root.gen("scheme");
26✔
680
        rc = curl_url_get(cu, CURLUPART_SCHEME, url_part.out(), 0);
26✔
681
        if (rc == CURLUE_OK) {
26✔
682
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
683
        } else {
UNCOV
684
            root.gen();
×
685
        }
686
        root.gen("username");
26✔
687
        rc = curl_url_get(cu, CURLUPART_USER, url_part.out(), CURLU_URLDECODE);
26✔
688
        if (rc == CURLUE_OK) {
26✔
689
            root.gen(string_fragment::from_c_str(url_part.in()));
5✔
690
        } else {
691
            root.gen();
21✔
692
        }
693
        root.gen("password");
26✔
694
        rc = curl_url_get(
26✔
695
            cu, CURLUPART_PASSWORD, url_part.out(), CURLU_URLDECODE);
696
        if (rc == CURLUE_OK) {
26✔
UNCOV
697
            root.gen(string_fragment::from_c_str(url_part.in()));
×
698
        } else {
699
            root.gen();
26✔
700
        }
701
        root.gen("host");
26✔
702
        rc = curl_url_get(cu, CURLUPART_HOST, url_part.out(), CURLU_URLDECODE);
26✔
703
        if (rc == CURLUE_OK) {
26✔
704
            root.gen(string_fragment::from_c_str(url_part.in()));
26✔
705
        } else {
UNCOV
706
            root.gen();
×
707
        }
708
        root.gen("port");
26✔
709
        rc = curl_url_get(cu, CURLUPART_PORT, url_part.out(), 0);
26✔
710
        if (rc == CURLUE_OK) {
26✔
UNCOV
711
            root.gen(string_fragment::from_c_str(url_part.in()));
×
712
        } else {
713
            root.gen();
26✔
714
        }
715
        root.gen("path");
26✔
716
        rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE);
26✔
717
        if (rc == CURLUE_OK) {
26✔
718
            auto path_frag = string_fragment::from_c_str(url_part.in());
26✔
719
            auto path_utf_res = is_utf8(path_frag);
26✔
720
            if (path_utf_res.is_valid()) {
26✔
721
                root.gen(path_frag);
26✔
722
            } else {
UNCOV
723
                rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), 0);
×
UNCOV
724
                if (rc == CURLUE_OK) {
×
UNCOV
725
                    root.gen(string_fragment::from_c_str(url_part.in()));
×
726
                } else {
UNCOV
727
                    root.gen();
×
728
                }
729
            }
730
        } else {
UNCOV
731
            root.gen();
×
732
        }
733
        rc = curl_url_get(cu, CURLUPART_QUERY, url_part.out(), 0);
26✔
734
        if (rc == CURLUE_OK) {
26✔
735
            root.gen("query");
14✔
736
            root.gen(string_fragment::from_c_str(url_part.in()));
14✔
737

738
            root.gen("parameters");
14✔
739
            robin_hood::unordered_set<std::string> seen_keys;
14✔
740
            yajlpp_map query_map(gen);
14✔
741

742
            for (size_t lpc = 0; url_part.in()[lpc]; lpc++) {
206✔
743
                if (url_part.in()[lpc] == '+') {
192✔
744
                    url_part.in()[lpc] = ' ';
1✔
745
                }
746
            }
747
            auto query_frag = string_fragment::from_c_str(url_part.in());
14✔
748
            auto remaining = query_frag;
14✔
749

750
            while (true) {
751
                auto split_res
752
                    = remaining.split_when(string_fragment::tag1{'&'});
27✔
753
                auto_mem<char> kv_pair(curl_free);
27✔
754
                auto kv_pair_encoded = split_res.first;
27✔
755
                int out_len = 0;
27✔
756

757
                kv_pair = curl_easy_unescape(CURL_HANDLE,
758
                                             kv_pair_encoded.data(),
759
                                             kv_pair_encoded.length(),
760
                                             &out_len);
27✔
761

762
                auto kv_pair_frag
763
                    = string_fragment::from_bytes(kv_pair.in(), out_len);
27✔
764
                auto eq_index_opt = kv_pair_frag.find('=');
27✔
765
                if (eq_index_opt) {
27✔
766
                    auto key = kv_pair_frag.sub_range(0, eq_index_opt.value());
11✔
767
                    auto val = kv_pair_frag.substr(eq_index_opt.value() + 1);
11✔
768

769
                    auto key_utf_res = is_utf8(key);
11✔
770
                    auto val_utf_res = is_utf8(val);
11✔
771
                    if (key_utf_res.is_valid()) {
11✔
772
                        auto key_str = key.to_string();
11✔
773

774
                        if (seen_keys.count(key_str) == 0) {
11✔
775
                            seen_keys.emplace(key_str);
11✔
776
                            query_map.gen(key);
11✔
777
                            if (val_utf_res.is_valid()) {
11✔
778
                                query_map.gen(val);
11✔
779
                            } else {
UNCOV
780
                                auto eq = strchr(kv_pair_encoded.data(), '=');
×
UNCOV
781
                                query_map.gen(
×
UNCOV
782
                                    string_fragment::from_c_str(eq + 1));
×
783
                            }
784
                        }
785
                    } else {
11✔
786
                    }
787
                } else {
788
                    auto val_str = split_res.first.to_string();
16✔
789

790
                    if (seen_keys.count(val_str) == 0) {
16✔
791
                        seen_keys.insert(val_str);
16✔
792
                        query_map.gen(split_res.first);
16✔
793
                        query_map.gen();
16✔
794
                    }
795
                }
16✔
796

797
                if (split_res.second.empty()) {
27✔
798
                    break;
14✔
799
                }
800

801
                remaining = split_res.second;
13✔
802
            }
40✔
803
        } else {
14✔
804
            root.gen("query");
12✔
805
            root.gen();
12✔
806
            root.gen("parameters");
12✔
807
            root.gen();
12✔
808
        }
809
        root.gen("fragment");
26✔
810
        rc = curl_url_get(
26✔
811
            cu, CURLUPART_FRAGMENT, url_part.out(), CURLU_URLDECODE);
812
        if (rc == CURLUE_OK) {
26✔
813
            root.gen(string_fragment::from_c_str(url_part.in()));
3✔
814
        } else {
815
            root.gen();
23✔
816
        }
817
    }
26✔
818

819
    return json_string(gen);
26✔
820
}
27✔
821

822
struct url_parts {
823
    std::optional<std::string> up_scheme;
824
    std::optional<std::string> up_username;
825
    std::optional<std::string> up_password;
826
    std::optional<std::string> up_host;
827
    std::optional<std::string> up_port;
828
    std::optional<std::string> up_path;
829
    std::optional<std::string> up_query;
830
    std::map<std::string, std::optional<std::string>> up_parameters;
831
    std::optional<std::string> up_fragment;
832
};
833

834
const typed_json_path_container<url_parts>&
835
get_url_parts_handlers()
13✔
836
{
837
    static const json_path_container url_params_handlers = {
838
        yajlpp::pattern_property_handler("(?<param>.*)")
13✔
839
            .for_field(&url_parts::up_parameters),
13✔
840
    };
52✔
841

842
    static const typed_json_path_container<url_parts> retval = {
843
        yajlpp::property_handler("scheme").for_field(&url_parts::up_scheme),
26✔
844
        yajlpp::property_handler("username").for_field(&url_parts::up_username),
26✔
845
        yajlpp::property_handler("password").for_field(&url_parts::up_password),
26✔
846
        yajlpp::property_handler("host").for_field(&url_parts::up_host),
26✔
847
        yajlpp::property_handler("port").for_field(&url_parts::up_port),
26✔
848
        yajlpp::property_handler("path").for_field(&url_parts::up_path),
26✔
849
        yajlpp::property_handler("query").for_field(&url_parts::up_query),
26✔
850
        yajlpp::property_handler("parameters")
26✔
851
            .with_children(url_params_handlers),
13✔
852
        yajlpp::property_handler("fragment").for_field(&url_parts::up_fragment),
26✔
853
    };
156✔
854

855
    return retval;
13✔
856
}
156✔
857

858
auto_mem<char>
859
sql_unparse_url(string_fragment in)
13✔
860
{
861
    static auto* CURL_HANDLE = get_curl_easy();
13✔
862
    static intern_string_t SRC = intern_string::lookup("arg");
39✔
863

864
    auto parse_res = get_url_parts_handlers().parser_for(SRC).of(in);
13✔
865
    if (parse_res.isErr()) {
13✔
866
        throw parse_res.unwrapErr()[0];
3✔
867
    }
868

869
    auto up = parse_res.unwrap();
10✔
870
    auto_mem<CURLU> cu(curl_url_cleanup);
10✔
871
    cu = curl_url();
10✔
872

873
    if (up.up_scheme) {
10✔
874
        curl_url_set(
9✔
875
            cu, CURLUPART_SCHEME, up.up_scheme->c_str(), CURLU_URLENCODE);
876
    }
877
    if (up.up_username) {
10✔
UNCOV
878
        curl_url_set(
×
879
            cu, CURLUPART_USER, up.up_username->c_str(), CURLU_URLENCODE);
880
    }
881
    if (up.up_password) {
10✔
UNCOV
882
        curl_url_set(
×
883
            cu, CURLUPART_PASSWORD, up.up_password->c_str(), CURLU_URLENCODE);
884
    }
885
    if (up.up_host) {
10✔
886
        curl_url_set(cu, CURLUPART_HOST, up.up_host->c_str(), CURLU_URLENCODE);
9✔
887
    }
888
    if (up.up_port) {
10✔
UNCOV
889
        curl_url_set(cu, CURLUPART_PORT, up.up_port->c_str(), 0);
×
890
    }
891
    if (up.up_path) {
10✔
892
        curl_url_set(cu, CURLUPART_PATH, up.up_path->c_str(), CURLU_URLENCODE);
4✔
893
    }
894
    if (up.up_query) {
10✔
895
        curl_url_set(cu, CURLUPART_QUERY, up.up_query->c_str(), 0);
4✔
896
    } else if (!up.up_parameters.empty()) {
6✔
UNCOV
897
        for (const auto& pair : up.up_parameters) {
×
UNCOV
898
            auto_mem<char> key(curl_free);
×
UNCOV
899
            auto_mem<char> value(curl_free);
×
UNCOV
900
            std::string qparam;
×
901

902
            key = curl_easy_escape(
UNCOV
903
                CURL_HANDLE, pair.first.c_str(), pair.first.length());
×
UNCOV
904
            if (pair.second) {
×
905
                value = curl_easy_escape(
UNCOV
906
                    CURL_HANDLE, pair.second->c_str(), pair.second->length());
×
UNCOV
907
                qparam = fmt::format(FMT_STRING("{}={}"), key.in(), value.in());
×
908
            } else {
UNCOV
909
                qparam = key.in();
×
910
            }
911

UNCOV
912
            curl_url_set(
×
913
                cu, CURLUPART_QUERY, qparam.c_str(), CURLU_APPENDQUERY);
914
        }
915
    }
916
    if (up.up_fragment) {
10✔
917
        curl_url_set(
1✔
918
            cu, CURLUPART_FRAGMENT, up.up_fragment->c_str(), CURLU_URLENCODE);
919
    }
920

921
    auto_mem<char> retval(curl_free);
10✔
922

923
    curl_url_get(cu, CURLUPART_URL, retval.out(), 0);
10✔
924
    return retval;
20✔
925
}
13✔
926

927
}  // namespace
928

929
json_string
930
extract(const char* str)
16✔
931
{
932
    data_scanner ds(str);
16✔
933
    data_parser dp(&ds);
16✔
934

935
    dp.parse();
16✔
936
    // dp.print(stderr, dp.dp_pairs);
937

938
    yajlpp_gen gen;
16✔
939
    yajl_gen_config(gen, yajl_gen_beautify, false);
16✔
940

941
    elements_to_json(gen, dp, &dp.dp_pairs);
16✔
942

943
    return json_string(gen);
32✔
944
}
16✔
945

946
static std::string
947
sql_humanize_id(string_fragment id)
7✔
948
{
949
    auto& vc = view_colors::singleton();
7✔
950
    auto attrs = vc.attrs_for_ident(id.data(), id.length());
7✔
951

952
    return fmt::format(FMT_STRING("\x1b[38;5;{}m{}\x1b[0m"),
14✔
953
                       // XXX attrs.ta_fg_color.value_or(COLOR_CYAN),
954
                       (int8_t) ansi_color::cyan,
7✔
955
                       id);
14✔
956
}
7✔
957

958
static std::string
959
sql_pretty_print(string_fragment in)
6✔
960
{
961
    data_scanner ds(in);
6✔
962
    pretty_printer pp(&ds, {});
6✔
963
    attr_line_t retval;
6✔
964

965
    pp.append_to(retval);
6✔
966

967
    return std::move(retval.get_string());
12✔
968
}
6✔
969

970
int
971
string_extension_functions(struct FuncDef** basic_funcs,
1,612✔
972
                           struct FuncDefAgg** agg_funcs)
973
{
974
    static struct FuncDef string_funcs[] = {
975
        sqlite_func_adapter<decltype(&regexp), regexp>::builder(
UNCOV
976
            help_text("regexp", "Test if a string matches a regular expression")
×
977
                .sql_function()
1,033✔
978
                .with_parameter({"re", "The regular expression to use"})
2,066✔
979
                .with_parameter({
2,066✔
980
                    "str",
981
                    "The string to test against the regular expression",
982
                })),
983

984
        sqlite_func_adapter<decltype(&regexp_match), regexp_match>::builder(
2,066✔
UNCOV
985
            help_text("regexp_match",
×
986
                      "Match a string against a regular expression and return "
987
                      "the capture groups as JSON.")
988
                .sql_function()
1,033✔
989
                .with_prql_path({"text", "regexp_match"})
1,033✔
990
                .with_parameter({"re", "The regular expression to use"})
2,066✔
991
                .with_parameter({
2,066✔
992
                    "str",
993
                    "The string to test against the regular expression",
994
                })
995
                .with_tags({"string", "regex"})
1,033✔
996
                .with_example({
1,033✔
997
                    "To capture the digits from the string '123'",
998
                    "SELECT regexp_match('(\\d+)', '123')",
999
                })
1000
                .with_example({
1,033✔
1001
                    "To capture a number and word into a JSON object with the "
1002
                    "properties 'col_0' and 'col_1'",
1003
                    "SELECT regexp_match('(\\d+) (\\w+)', '123 four')",
1004
                })
1005
                .with_example({
2,066✔
1006
                    "To capture a number and word into a JSON object with the "
1007
                    "named properties 'num' and 'str'",
1008
                    "SELECT regexp_match('(?<num>\\d+) (?<str>\\w+)', '123 "
1009
                    "four')",
1010
                }))
1011
            .with_result_subtype(),
1012

1013
        sqlite_func_adapter<decltype(&regexp_replace), regexp_replace>::builder(
UNCOV
1014
            help_text("regexp_replace",
×
1015
                      "Replace the parts of a string that match a regular "
1016
                      "expression.")
1017
                .sql_function()
1,033✔
1018
                .with_prql_path({"text", "regexp_replace"})
1,033✔
1019
                .with_parameter(
2,066✔
1020
                    {"str", "The string to perform replacements on"})
1021
                .with_parameter({"re", "The regular expression to match"})
2,066✔
1022
                .with_parameter({
2,066✔
1023
                    "repl",
1024
                    "The replacement string.  "
1025
                    "You can reference capture groups with a "
1026
                    "backslash followed by the number of the "
1027
                    "group, starting with 1.",
1028
                })
1029
                .with_tags({"string", "regex"})
1,033✔
1030
                .with_example({
1,033✔
1031
                    "To replace the word at the start of the string "
1032
                    "'Hello, World!' with 'Goodbye'",
1033
                    "SELECT regexp_replace('Hello, World!', "
1034
                    "'^(\\w+)', 'Goodbye')",
1035
                })
1036
                .with_example({
2,066✔
1037
                    "To wrap alphanumeric words with angle brackets",
1038
                    "SELECT regexp_replace('123 abc', '(\\w+)', '<\\1>')",
1039
                })),
1040

1041
        sqlite_func_adapter<decltype(&sql_humanize_file_size),
1042
                            sql_humanize_file_size>::
1043
            builder(help_text(
×
1044
                        "humanize_file_size",
1045
                        "Format the given file size as a human-friendly string")
1046
                        .sql_function()
1,033✔
1047
                        .with_prql_path({"humanize", "file_size"})
1,033✔
1048
                        .with_parameter({"value", "The file size to format"})
2,066✔
1049
                        .with_tags({"string"})
1,033✔
1050
                        .with_example({
2,066✔
1051
                            "To format an amount",
1052
                            "SELECT humanize_file_size(10 * 1024 * 1024)",
1053
                        })),
1054

1055
        sqlite_func_adapter<decltype(&sql_humanize_id), sql_humanize_id>::
UNCOV
1056
            builder(help_text("humanize_id",
×
1057
                              "Colorize the given ID using ANSI escape codes.")
1058
                        .sql_function()
1,033✔
1059
                        .with_prql_path({"humanize", "id"})
1,033✔
1060
                        .with_parameter({"id", "The identifier to color"})
2,066✔
1061
                        .with_tags({"string"})
1,033✔
1062
                        .with_example({
2,066✔
1063
                            "To colorize the ID 'cluster1'",
1064
                            "SELECT humanize_id('cluster1')",
1065
                        })),
1066

1067
        sqlite_func_adapter<decltype(&humanize::sparkline),
1068
                            humanize::sparkline>::
1069
            builder(
UNCOV
1070
                help_text("sparkline",
×
1071
                          "Function used to generate a sparkline bar chart.  "
1072
                          "The non-aggregate version converts a single numeric "
1073
                          "value on a range to a bar chart character.  The "
1074
                          "aggregate version returns a string with a bar "
1075
                          "character for every numeric input")
1076
                    .sql_function()
1,033✔
1077
                    .with_prql_path({"text", "sparkline"})
1,033✔
1078
                    .with_parameter({"value", "The numeric value to convert"})
2,066✔
1079
                    .with_parameter(help_text("upper",
3,099✔
1080
                                              "The upper bound of the numeric "
1081
                                              "range.  The non-aggregate "
1082
                                              "version defaults to 100.  The "
1083
                                              "aggregate version uses the "
1084
                                              "largest value in the inputs.")
1085
                                        .optional())
1,033✔
1086
                    .with_tags({"string"})
1,033✔
1087
                    .with_example({
1,033✔
1088
                        "To get the unicode block element for the "
1089
                        "value 32 in the "
1090
                        "range of 0-128",
1091
                        "SELECT sparkline(32, 128)",
1092
                    })
1093
                    .with_example({
2,066✔
1094
                        "To chart the values in a JSON array",
1095
                        "SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, "
1096
                        "4, 5, 6, 7, 8]')",
1097
                    })),
1098

1099
        sqlite_func_adapter<decltype(&sql_anonymize), sql_anonymize>::builder(
UNCOV
1100
            help_text("anonymize",
×
1101
                      "Replace identifying information with random values.")
1102
                .sql_function()
1,033✔
1103
                .with_prql_path({"text", "anonymize"})
1,033✔
1104
                .with_parameter({"value", "The text to anonymize"})
2,066✔
1105
                .with_tags({"string"})
1,033✔
1106
                .with_example({
2,066✔
1107
                    "To anonymize an IP address",
1108
                    "SELECT anonymize('Hello, 192.168.1.2')",
1109
                })),
1110

1111
        sqlite_func_adapter<decltype(&extract), extract>::builder(
2,066✔
UNCOV
1112
            help_text("extract",
×
1113
                      "Automatically Parse and extract data from a string")
1114
                .sql_function()
1,033✔
1115
                .with_prql_path({"text", "discover"})
1,033✔
1116
                .with_parameter({"str", "The string to parse"})
2,066✔
1117
                .with_tags({"string"})
1,033✔
1118
                .with_example({
1,033✔
1119
                    "To extract key/value pairs from a string",
1120
                    "SELECT extract('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1121
                })
1122
                .with_example({
2,066✔
1123
                    "To extract columnar data from a string",
1124
                    "SELECT extract('1.0 abc 2.0')",
1125
                }))
1126
            .with_result_subtype(),
1127

1128
        sqlite_func_adapter<decltype(&logfmt2json), logfmt2json>::builder(
2,066✔
1129
            help_text("logfmt2json",
1,033✔
1130
                      "Convert a logfmt-encoded string into JSON")
1131
                .sql_function()
1,033✔
1132
                .with_prql_path({"logfmt", "to_json"})
1,033✔
1133
                .with_parameter({"str", "The logfmt message to parse"})
2,066✔
1134
                .with_tags({"string"})
1,033✔
1135
                .with_example({
2,066✔
1136
                    "To extract key/value pairs from a log message",
1137
                    "SELECT logfmt2json('foo=1 bar=2 name=\"Rolo Tomassi\"')",
1138
                }))
1139
            .with_result_subtype(),
1140

1141
        sqlite_func_adapter<
1142
            decltype(static_cast<bool (*)(const char*, const char*)>(
1143
                &startswith)),
1144
            startswith>::
UNCOV
1145
            builder(help_text("startswith",
×
1146
                              "Test if a string begins with the given prefix")
1147
                        .sql_function()
1,033✔
1148
                        .with_parameter({"str", "The string to test"})
2,066✔
1149
                        .with_parameter(
2,066✔
1150
                            {"prefix", "The prefix to check in the string"})
1151
                        .with_tags({"string"})
1,033✔
1152
                        .with_example({
1,033✔
1153
                            "To test if the string 'foobar' starts with 'foo'",
1154
                            "SELECT startswith('foobar', 'foo')",
1155
                        })
1156
                        .with_example({
2,066✔
1157
                            "To test if the string 'foobar' starts with 'bar'",
1158
                            "SELECT startswith('foobar', 'bar')",
1159
                        })),
1160

1161
        sqlite_func_adapter<decltype(static_cast<bool (*)(
1162
                                         const char*, const char*)>(&endswith)),
1163
                            endswith>::
1164
            builder(
1165
                help_text("endswith",
×
1166
                          "Test if a string ends with the given suffix")
1167
                    .sql_function()
1,033✔
1168
                    .with_parameter({"str", "The string to test"})
2,066✔
1169
                    .with_parameter(
2,066✔
1170
                        {"suffix", "The suffix to check in the string"})
1171
                    .with_tags({"string"})
1,033✔
1172
                    .with_example({
1,033✔
1173
                        "To test if the string 'notbad.jpg' ends with '.jpg'",
1174
                        "SELECT endswith('notbad.jpg', '.jpg')",
1175
                    })
1176
                    .with_example({
2,066✔
1177
                        "To test if the string 'notbad.png' starts with '.jpg'",
1178
                        "SELECT endswith('notbad.png', '.jpg')",
1179
                    })),
1180

1181
        sqlite_func_adapter<decltype(&sql_fuzzy_match), sql_fuzzy_match>::
UNCOV
1182
            builder(help_text(
×
1183
                        "fuzzy_match",
1184
                        "Perform a fuzzy match of a pattern against a "
1185
                        "string and return a score or NULL if the pattern was "
1186
                        "not matched")
1187
                        .sql_function()
1,033✔
1188
                        .with_parameter(help_text(
2,066✔
1189
                            "pattern", "The pattern to look for in the string"))
1190
                        .with_parameter(
1,033✔
1191
                            help_text("str", "The string to match against"))
2,066✔
1192
                        .with_tags({"string"})
1,033✔
1193
                        .with_example({
2,066✔
1194
                            "To match the pattern 'fo' against 'filter-out'",
1195
                            "SELECT fuzzy_match('fo', 'filter-out')",
1196
                        })),
1197

1198
        sqlite_func_adapter<decltype(&spooky_hash), spooky_hash>::builder(
1199
            help_text("spooky_hash",
×
1200
                      "Compute the hash value for the given arguments.")
1201
                .sql_function()
1,033✔
1202
                .with_parameter(
1,033✔
1203
                    help_text("str", "The string to hash").one_or_more())
2,066✔
1204
                .with_tags({"string"})
1,033✔
1205
                .with_example({
1,033✔
1206
                    "To produce a hash for the string 'Hello, World!'",
1207
                    "SELECT spooky_hash('Hello, World!')",
1208
                })
1209
                .with_example({
1,033✔
1210
                    "To produce a hash for the parameters where one is NULL",
1211
                    "SELECT spooky_hash('Hello, World!', NULL)",
1212
                })
1213
                .with_example({
1,033✔
1214
                    "To produce a hash for the parameters where one "
1215
                    "is an empty string",
1216
                    "SELECT spooky_hash('Hello, World!', '')",
1217
                })
1218
                .with_example({
2,066✔
1219
                    "To produce a hash for the parameters where one "
1220
                    "is a number",
1221
                    "SELECT spooky_hash('Hello, World!', 123)",
1222
                })),
1223

1224
        sqlite_func_adapter<decltype(&sql_gunzip), sql_gunzip>::builder(
UNCOV
1225
            help_text("gunzip", "Decompress a gzip file")
×
1226
                .sql_function()
1,033✔
1227
                .with_parameter(
1,033✔
1228
                    help_text("b", "The blob to decompress").one_or_more())
2,066✔
1229
                .with_tags({"string"})),
2,066✔
1230

1231
        sqlite_func_adapter<decltype(&sql_gzip), sql_gzip>::builder(
UNCOV
1232
            help_text("gzip", "Compress a string into a gzip file")
×
1233
                .sql_function()
1,033✔
1234
                .with_parameter(
1,033✔
1235
                    help_text("value", "The value to compress").one_or_more())
2,066✔
1236
                .with_tags({"string"})),
2,066✔
1237

1238
        sqlite_func_adapter<decltype(&sql_encode), sql_encode>::builder(
UNCOV
1239
            help_text("encode", "Encode the value using the given algorithm")
×
1240
                .sql_function()
1,033✔
1241
                .with_parameter(help_text("value", "The value to encode"))
2,066✔
1242
                .with_parameter(help_text("algorithm",
2,066✔
1243
                                          "One of the following encoding "
1244
                                          "algorithms: base64, hex, uri"))
1245
                .with_tags({"string"})
1,033✔
1246
                .with_example({
1,033✔
1247
                    "To base64-encode 'Hello, World!'",
1248
                    "SELECT encode('Hello, World!', 'base64')",
1249
                })
1250
                .with_example({
1,033✔
1251
                    "To hex-encode 'Hello, World!'",
1252
                    "SELECT encode('Hello, World!', 'hex')",
1253
                })
1254
                .with_example({
2,066✔
1255
                    "To URI-encode 'Hello, World!'",
1256
                    "SELECT encode('Hello, World!', 'uri')",
1257
                })),
1258

1259
        sqlite_func_adapter<decltype(&sql_decode), sql_decode>::builder(
UNCOV
1260
            help_text("decode", "Decode the value using the given algorithm")
×
1261
                .sql_function()
1,033✔
1262
                .with_parameter(help_text("value", "The value to decode"))
2,066✔
1263
                .with_parameter(help_text("algorithm",
2,066✔
1264
                                          "One of the following encoding "
1265
                                          "algorithms: base64, hex, uri"))
1266
                .with_tags({"string"})
1,033✔
1267
                .with_example({
2,066✔
1268
                    "To decode the URI-encoded string '%63%75%72%6c'",
1269
                    "SELECT decode('%63%75%72%6c', 'uri')",
1270
                })),
1271

1272
        sqlite_func_adapter<decltype(&sql_parse_url), sql_parse_url>::builder(
2,066✔
UNCOV
1273
            help_text("parse_url",
×
1274
                      "Parse a URL and return the components in a JSON object. "
1275
                      "Limitations: not all URL schemes are supported and "
1276
                      "repeated query parameters are not captured.")
1277
                .sql_function()
1,033✔
1278
                .with_parameter(help_text("url", "The URL to parse"))
2,066✔
1279
                .with_result({
2,066✔
1280
                    "scheme",
1281
                    "The URL's scheme",
1282
                })
1283
                .with_result({
2,066✔
1284
                    "username",
1285
                    "The name of the user specified in the URL",
1286
                })
1287
                .with_result({
2,066✔
1288
                    "password",
1289
                    "The password specified in the URL",
1290
                })
1291
                .with_result({
2,066✔
1292
                    "host",
1293
                    "The host name / IP specified in the URL",
1294
                })
1295
                .with_result({
2,066✔
1296
                    "port",
1297
                    "The port specified in the URL",
1298
                })
1299
                .with_result({
2,066✔
1300
                    "path",
1301
                    "The path specified in the URL",
1302
                })
1303
                .with_result({
2,066✔
1304
                    "query",
1305
                    "The query string in the URL",
1306
                })
1307
                .with_result({
2,066✔
1308
                    "parameters",
1309
                    "An object containing the query parameters",
1310
                })
1311
                .with_result({
2,066✔
1312
                    "fragment",
1313
                    "The fragment specified in the URL",
1314
                })
1315
                .with_tags({"string", "url"})
1,033✔
1316
                .with_example({
1,033✔
1317
                    "To parse the URL "
1318
                    "'https://example.com/search?q=hello%20world'",
1319
                    "SELECT "
1320
                    "parse_url('https://example.com/search?q=hello%20world')",
1321
                })
1322
                .with_example({
2,066✔
1323
                    "To parse the URL "
1324
                    "'https://alice@[fe80::14ff:4ee5:1215:2fb2]'",
1325
                    "SELECT "
1326
                    "parse_url('https://alice@[fe80::14ff:4ee5:1215:2fb2]')",
1327
                }))
1328
            .with_result_subtype(),
1329

1330
        sqlite_func_adapter<decltype(&sql_unparse_url), sql_unparse_url>::
1331
            builder(
UNCOV
1332
                help_text("unparse_url",
×
1333
                          "Convert a JSON object containing the parts of a "
1334
                          "URL into a URL string")
1335
                    .sql_function()
1,033✔
1336
                    .with_parameter(help_text(
2,066✔
1337
                        "obj", "The JSON object containing the URL parts"))
1338
                    .with_tags({"string", "url"})
1,033✔
1339
                    .with_example({
2,066✔
1340
                        "To unparse the object "
1341
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1342
                        "SELECT "
1343
                        "unparse_url('{\"scheme\": \"https\", \"host\": "
1344
                        "\"example.com\"}')",
1345
                    })),
1346

1347
        sqlite_func_adapter<decltype(&sql_pretty_print), sql_pretty_print>::
1348
            builder(
UNCOV
1349
                help_text("pretty_print", "Pretty-print the given string")
×
1350
                    .sql_function()
1,033✔
1351
                    .with_prql_path({"text", "pretty"})
1,033✔
1352
                    .with_parameter(help_text("str", "The string to format"))
2,066✔
1353
                    .with_tags({"string"})
1,033✔
1354
                    .with_example({
2,066✔
1355
                        "To pretty-print the string "
1356
                        "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
1357
                        "SELECT "
1358
                        "pretty_print('{\"scheme\": \"https\", \"host\": "
1359
                        "\"example.com\"}')",
1360
                    })),
1361

1362
        {nullptr},
1363
    };
41,899✔
1364

1365
    static struct FuncDefAgg str_agg_funcs[] = {
1366
        {
1367
            "group_spooky_hash",
1368
            -1,
1369
            SQLITE_UTF8,
1370
            0,
1371
            sql_spooky_hash_step,
1372
            sql_spooky_hash_final,
1373
            help_text("group_spooky_hash",
1,033✔
1374
                      "Compute the hash value for the given arguments")
1375
                .sql_agg_function()
1,033✔
1376
                .with_parameter(
1,033✔
1377
                    help_text("str", "The string to hash").one_or_more())
2,066✔
1378
                .with_tags({"string"})
1,033✔
1379
                .with_example({
2,066✔
1380
                    "To produce a hash of all of the values of 'column1'",
1381
                    "SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), "
1382
                    "('123'))",
1383
                }),
1384
        },
1385

1386
        {
1387
            "sparkline",
1388
            -1,
1389
            SQLITE_UTF8,
1390
            0,
1391
            sparkline_step,
1392
            sparkline_final,
1393
        },
1394

1395
        {nullptr},
1396
    };
2,645✔
1397

1398
    *basic_funcs = string_funcs;
1,612✔
1399
    *agg_funcs = str_agg_funcs;
1,612✔
1400

1401
    return SQLITE_OK;
1,612✔
1402
}
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