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

tstack / lnav / 18378041721-2564

09 Oct 2025 01:27PM UTC coverage: 70.133% (-0.08%) from 70.213%
18378041721-2564

push

github

tstack
[db] move lnav tables to a separate DB

Related to #1525

156 of 292 new or added lines in 11 files covered. (53.42%)

43 existing lines in 6 files now uncovered.

50274 of 71684 relevant lines covered (70.13%)

412914.39 hits per line

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

54.55
/src/sql_commands.cc
1
/**
2
 * Copyright (c) 2021, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <atomic>
31
#include <chrono>
32
#include <string>
33
#include <thread>
34
#include <vector>
35

36
#include <stdio.h>
37
#include <stdlib.h>
38

39
#include "base/auto_mem.hh"
40
#include "base/fs_util.hh"
41
#include "base/injector.bind.hh"
42
#include "base/injector.hh"
43
#include "base/itertools.hh"
44
#include "base/lnav.console.hh"
45
#include "base/lnav_log.hh"
46
#include "base/opt_util.hh"
47
#include "base/progress.hh"
48
#include "base/result.h"
49
#include "bound_tags.hh"
50
#include "command_executor.hh"
51
#include "config.h"
52
#include "fmt/chrono.h"
53
#include "lnav.hh"
54
#include "lnav_util.hh"
55
#include "readline_context.hh"
56
#include "safe/safe.h"
57
#include "service_tags.hh"
58
#include "shlex.hh"
59
#include "sql_help.hh"
60
#include "sqlite-extension-func.hh"
61
#include "sqlitepp.client.hh"
62
#include "sqlitepp.hh"
63
#include "view_helpers.hh"
64

65
using namespace std::chrono_literals;
66

67
static Result<std::string, lnav::console::user_message>
68
sql_cmd_dump(exec_context& ec,
1✔
69
             std::string cmdline,
70
             std::vector<std::string>& args)
71
{
72
    static auto& lnav_db = injector::get<auto_sqlite3&>();
1✔
73
    static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
1✔
74

75
    std::string retval;
1✔
76

77
    if (args.empty()) {
1✔
78
        args.emplace_back("filename");
×
79
        args.emplace_back("tables");
×
80
        return Ok(retval);
×
81
    }
82

83
    if (args.size() < 2) {
1✔
84
        return ec.make_error("expecting a file name to write to");
×
85
    }
86

87
    if (args.size() < 3) {
1✔
88
        return ec.make_error("expecting a table name to dump");
×
89
    }
90

91
    if (lnav_flags & LNF_SECURE_MODE) {
1✔
92
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
93
    }
94

95
    auto_mem<FILE> file(fclose);
1✔
96

97
    if ((file = fopen(args[1].c_str(), "w+")) == nullptr) {
1✔
98
        return ec.make_error(
99
            "unable to open '{}' for writing: {}", args[1], strerror(errno));
×
100
    }
101

102
    auto prep_res
103
        = prepare_stmt(lnav_db, "SELECT name FROM pragma_database_list");
1✔
104
    if (prep_res.isErr()) {
1✔
105
        return ec.make_error("unable to prepare database_list: {}",
NEW
106
                             prep_res.unwrapErr());
×
107
    }
108
    auto stmt = prep_res.unwrap();
1✔
109
    auto all_db_names = std::vector<std::string>();
1✔
110
    stmt.for_each_row<std::string>([&all_db_names](auto row) {
1✔
111
        all_db_names.emplace_back(row);
2✔
112
        return false;
2✔
113
    });
114

115
    for (size_t lpc = 2; lpc < args.size(); lpc++) {
2✔
116
        auto arg = string_fragment::from_str(args[lpc]);
1✔
117
        auto [schema_name, table_name]
1✔
118
            = arg.split_when(string_fragment::tag1{'.'});
1✔
119
        auto db_names = table_name.empty()
1✔
120
            ? all_db_names
1✔
121
            : std::vector{schema_name.to_string()};
1✔
122

123
        for (const auto& db_name : db_names) {
3✔
124
            sqlite3_db_dump(lnav_db.in(),
2✔
125
                            db_name.c_str(),
126
                            args[lpc].c_str(),
2✔
127
                            (int (*)(const char*, void*)) fputs,
128
                            file.in());
2✔
129
        }
130
    }
1✔
131

132
    auto table_count = args.size() - 2;
1✔
133
    retval = fmt::format(
1✔
134
        FMT_STRING("info: wrote {} table(s) to {}"), table_count, args[1]);
4✔
135
    return Ok(retval);
1✔
136
}
1✔
137

138
struct backup_progress_t {
139
    std::atomic<lnav::progress_status_t> bp_status{
140
        lnav::progress_status_t::idle};
141
    std::atomic_uint32_t bp_instance{0};
142
    std::atomic_uint64_t bp_complete{0};
143
    std::atomic_uint64_t bp_total{0};
144
    safe::Safe<std::vector<lnav::console::user_message>> bp_messages;
145

146
    struct backup_guard {
147
        ~backup_guard()
2✔
148
        {
149
            if (bg_helper.gh_enabled) {
2✔
150
                INSTANCE.bp_status.store(lnav::progress_status_t::idle);
2✔
151
            }
152
        }
2✔
153

154
        void push_msg(lnav::console::user_message um)
1✔
155
        {
156
            log_info("backup message: %s", um.to_attr_line().al_string.c_str());
3✔
157
            INSTANCE.bp_messages.writeAccess()->emplace_back(std::move(um));
1✔
158
        }
1✔
159

160
        void update(uint64_t complete, uint64_t total)
1✔
161
        {
162
            INSTANCE.bp_complete.store(complete);
163
            INSTANCE.bp_total.store(total);
164
        }
1✔
165

166
        lnav::guard_helper bg_helper;
167
    };
168

169
    static backup_guard begin()
2✔
170
    {
171
        INSTANCE.bp_messages.writeAccess()->clear();
2✔
172
        INSTANCE.bp_complete.store(0);
173
        INSTANCE.bp_total.store(1);
174
        INSTANCE.bp_instance.fetch_add(1, std::memory_order_relaxed);
175
        INSTANCE.bp_status.store(lnav::progress_status_t::working);
2✔
176

177
        return {};
2✔
178
    }
179

180
    static backup_progress_t INSTANCE;
181
};
182

183
backup_progress_t backup_progress_t::INSTANCE;
184

185
static lnav::task_progress
186
backup_prog_rep()
636✔
187
{
188
    return {
189
        ";.save",
190
        backup_progress_t::INSTANCE.bp_status.load(),
636✔
191
        backup_progress_t::INSTANCE.bp_instance.load(),
636✔
192
        "Backing up DB",
193
        backup_progress_t::INSTANCE.bp_complete.load(),
636✔
194
        backup_progress_t::INSTANCE.bp_total.load(),
636✔
195
        *backup_progress_t::INSTANCE.bp_messages.readAccess(),
1,272✔
196
    };
636✔
197
}
5,088✔
198

199
DIST_SLICE(prog_reps) lnav::progress_reporter_t backup_rep = backup_prog_rep;
200

201
static void
202
backup_user_db(const std::string& filename)
2✔
203
{
204
    static auto op = lnav_operation{__FUNCTION__};
2✔
205
    auto op_guard = lnav_opid_guard::internal(op);
2✔
206
    log_info("starting backup of user DB: %s", filename.c_str());
2✔
207
    auto bguard = backup_progress_t::begin();
2✔
208

209
    auto_sqlite3 out_db;
2✔
210
    if (sqlite3_open(filename.c_str(), out_db.out()) != SQLITE_OK) {
2✔
NEW
211
        auto um = lnav::console::user_message::error(
×
212
                      attr_line_t("unable to open output DB: ")
1✔
213
                          .append(lnav::roles::file(filename)))
2✔
214
                      .with_reason(sqlite3_errmsg(out_db.in()));
1✔
215
        bguard.push_msg(um);
1✔
216
        return;
1✔
217
    }
1✔
218

219
    auto_sqlite3 db;
1✔
220
    if (sqlite3_open("file:user_db?mode=memory&cache=shared", db.out())
1✔
221
        != SQLITE_OK)
1✔
222
    {
NEW
223
        auto um = lnav::console::user_message::error(
×
224
                      "unable to open lnav's user DB")
NEW
225
                      .with_reason(sqlite3_errmsg(db.in()));
×
NEW
226
        bguard.push_msg(um);
×
NEW
227
        return;
×
228
    }
229

230
    {
231
        auto stmt = prepare_stmt(db, LNAV_ATTACH_DB).unwrap();
1✔
232
        auto exec_res = stmt.execute();
1✔
233
        if (exec_res.isErr()) {
1✔
234
            auto um
NEW
235
                = lnav::console::user_message::error("unable to attach lnav_db")
×
NEW
236
                      .with_reason(sqlite3_errmsg(db.in()));
×
NEW
237
            bguard.push_msg(um);
×
NEW
238
            return;
×
239
        }
240
    }
1✔
241

242
    auto* backup = sqlite3_backup_init(out_db.in(), "main", db.in(), "main");
1✔
243
    if (backup == nullptr) {
1✔
NEW
244
        auto um = lnav::console::user_message::error("unable to backup user DB")
×
NEW
245
                      .with_reason(sqlite3_errmsg(db.in()));
×
NEW
246
        bguard.push_msg(um);
×
NEW
247
        return;
×
248
    }
249

250
    auto done = false;
1✔
251
    auto loop_count = 0;
1✔
252
    while (!done) {
3✔
253
        auto rc = sqlite3_backup_step(backup, 1);
2✔
254
        switch (rc) {
2✔
255
            case SQLITE_DONE:
1✔
256
                done = true;
1✔
257
                break;
1✔
258
            case SQLITE_OK:
1✔
259
                if (loop_count % 100 == 0) {
1✔
260
                    std::this_thread::sleep_for(1ms);
1✔
261
                }
262
                break;
1✔
NEW
263
            case SQLITE_BUSY:
×
264
            case SQLITE_LOCKED:
NEW
265
                std::this_thread::sleep_for(10ms);
×
NEW
266
                break;
×
NEW
267
            default: {
×
NEW
268
                auto um = lnav::console::user_message::error(
×
269
                              "unable to backup user DB")
NEW
270
                              .with_reason(sqlite3_errmsg(db.in()));
×
NEW
271
                bguard.push_msg(um);
×
NEW
272
                sqlite3_backup_finish(backup);
×
NEW
273
                return;
×
274
            }
275
        }
276

277
        if (loop_count % 100 == 0) {
2✔
278
            auto total = sqlite3_backup_pagecount(backup);
1✔
279
            auto complete = total - sqlite3_backup_remaining(backup);
1✔
280
            bguard.update(complete, total);
1✔
281
        }
282

283
        loop_count += 1;
2✔
284
    }
285

286
    sqlite3_backup_finish(backup);
1✔
287
    log_info("backup complete");
1✔
288
}
4✔
289

290
static Result<std::string, lnav::console::user_message>
291
sql_cmd_save(exec_context& ec,
2✔
292
             std::string cmdline,
293
             std::vector<std::string>& args)
294
{
295
    static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
2✔
296
    std::string retval;
2✔
297

298
    if (args.empty()) {
2✔
NEW
299
        args.emplace_back("filename");
×
NEW
300
        return Ok(retval);
×
301
    }
302

303
    if (args.size() < 2) {
2✔
NEW
304
        return ec.make_error("expecting a file name to write to");
×
305
    }
306

307
    if (lnav_flags & LNF_SECURE_MODE) {
2✔
NEW
308
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
309
    }
310

311
    isc::to<bg_looper&, services::background_t>().send(
2✔
312
        [filename = args[1]](auto& bg_loop) { backup_user_db(filename); });
6✔
313

314
    return Ok(retval);
2✔
315
}
2✔
316

317
static Result<std::string, lnav::console::user_message>
318
sql_cmd_read(exec_context& ec,
2✔
319
             std::string cmdline,
320
             std::vector<std::string>& args)
321
{
322
    static const intern_string_t SRC = intern_string::lookup("cmdline");
6✔
323
    static auto& lnav_db = injector::get<auto_sqlite3&>();
2✔
324
    static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
2✔
325

326
    std::string retval;
2✔
327

328
    if (args.empty()) {
2✔
329
        args.emplace_back("filename");
×
330
        return Ok(retval);
×
331
    }
332

333
    if (lnav_flags & LNF_SECURE_MODE) {
2✔
334
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
335
    }
336

337
    shlex lexer(cmdline);
2✔
338

339
    auto split_args_res = lexer.split(ec.create_resolver());
2✔
340
    if (split_args_res.isErr()) {
2✔
341
        auto split_err = split_args_res.unwrapErr();
×
342
        auto um
343
            = lnav::console::user_message::error("unable to parse file name")
×
344
                  .with_reason(split_err.se_error.te_msg)
×
345
                  .with_snippet(lnav::console::snippet::from(
×
346
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
347
                  .move();
×
348

349
        return Err(um);
×
350
    }
351

352
    auto split_args = split_args_res.unwrap()
4✔
353
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
8✔
354

355
    for (size_t lpc = 1; lpc < split_args.size(); lpc++) {
3✔
356
        auto read_res = lnav::filesystem::read_file(split_args[lpc]);
2✔
357

358
        if (read_res.isErr()) {
2✔
359
            return ec.make_error("unable to read script file: {} -- {}",
360
                                 split_args[lpc],
1✔
361
                                 read_res.unwrapErr());
2✔
362
        }
363

364
        auto script = read_res.unwrap();
1✔
365
        auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
1✔
366
        const char* start = script.c_str();
1✔
367

368
        do {
369
            const char* tail;
370
            auto rc = sqlite3_prepare_v2(
2✔
371
                lnav_db.in(), start, -1, stmt.out(), &tail);
372

373
            if (rc != SQLITE_OK) {
2✔
374
                const char* errmsg = sqlite3_errmsg(lnav_db.in());
×
375

376
                return ec.make_error("{}", errmsg);
×
377
            }
378

379
            if (stmt.in() != nullptr) {
2✔
380
                std::string alt_msg;
2✔
381
                auto exec_res = execute_sql(
382
                    ec, std::string(start, tail - start), alt_msg);
2✔
383
                if (exec_res.isErr()) {
2✔
384
                    return exec_res;
×
385
                }
386
            }
2✔
387

388
            start = tail;
2✔
389
        } while (start[0]);
2✔
390
    }
2✔
391

392
    return Ok(retval);
1✔
393
}
2✔
394

395
static Result<std::string, lnav::console::user_message>
396
sql_cmd_schema(exec_context& ec,
1✔
397
               std::string cmdline,
398
               std::vector<std::string>& args)
399
{
400
    std::string retval;
1✔
401

402
    if (args.empty()) {
1✔
403
        args.emplace_back("sql-table");
×
404
        return Ok(retval);
×
405
    }
406

407
    ensure_view(LNV_SCHEMA);
1✔
408
    if (args.size() == 2) {
1✔
409
        const auto id = text_anchors::to_anchor_string(args[1]);
×
410
        auto* tss = lnav_data.ld_views[LNV_SCHEMA].get_sub_source();
×
411
        if (auto* ta = dynamic_cast<text_anchors*>(tss); ta != nullptr) {
×
412
            ta->row_for_anchor(id) |
×
413
                [](auto row) { lnav_data.ld_views[LNV_SCHEMA].set_top(row); };
×
414
        }
415
    }
416

417
    return Ok(retval);
1✔
418
}
1✔
419

420
static Result<std::string, lnav::console::user_message>
421
sql_cmd_msgformats(exec_context& ec,
1✔
422
                   std::string cmdline,
423
                   std::vector<std::string>& args)
424
{
425
    static const std::string MSG_FORMAT_STMT = R"(
3✔
426
SELECT count(*) AS total,
427
       min(log_line) AS log_line,
428
       min(log_time) AS log_time,
429
       humanize_duration(timediff(max(log_time), min(log_time))) AS duration,
430
       group_concat(DISTINCT log_format) AS log_formats,
431
       log_msg_format
432
    FROM all_logs
433
    WHERE log_msg_format != ''
434
    GROUP BY log_msg_format
435
    HAVING total > 1
436
    ORDER BY total DESC, log_line ASC
437
)";
438

439
    std::string retval;
1✔
440

441
    if (args.empty()) {
1✔
442
        return Ok(retval);
×
443
    }
444

445
    std::string alt;
1✔
446

447
    return execute_sql(ec, MSG_FORMAT_STMT, alt);
1✔
448
}
1✔
449

450
static Result<std::string, lnav::console::user_message>
451
sql_cmd_generic(exec_context& ec,
×
452
                std::string cmdline,
453
                std::vector<std::string>& args)
454
{
455
    std::string retval;
×
456

457
    if (args.empty()) {
×
458
        args.emplace_back("*");
×
459
        return Ok(retval);
×
460
    }
461

462
    return Ok(retval);
×
463
}
464

465
static Result<std::string, lnav::console::user_message>
466
prql_cmd_from(exec_context& ec,
×
467
              std::string cmdline,
468
              std::vector<std::string>& args)
469
{
470
    std::string retval;
×
471

472
    if (args.empty()) {
×
473
        args.emplace_back("prql-table");
×
474
        return Ok(retval);
×
475
    }
476

477
    return Ok(retval);
×
478
}
479

480
static readline_context::prompt_result_t
481
prql_cmd_from_prompt(exec_context& ec, const std::string& cmdline)
×
482
{
483
    if (!endswith(cmdline, "from ")) {
×
484
        return {};
×
485
    }
486

487
    auto* tc = *lnav_data.ld_view_stack.top();
×
488
    auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source());
×
489
    auto sel = tc->get_selection();
×
490

491
    if (!sel || lss == nullptr || lss->text_line_count() == 0) {
×
492
        return {};
×
493
    }
494

495
    auto line_pair = lss->find_line_with_file(lss->at(sel.value()));
×
496
    if (!line_pair) {
×
497
        return {};
×
498
    }
499

500
    auto format_name
501
        = line_pair->first->get_format_ptr()->get_name().to_string();
×
502
    return {
503
        "",
504
        lnav::prql::quote_ident(format_name) + " ",
×
505
    };
506
}
507

508
static Result<std::string, lnav::console::user_message>
509
prql_cmd_aggregate(exec_context& ec,
×
510
                   std::string cmdline,
511
                   std::vector<std::string>& args)
512
{
513
    std::string retval;
×
514

515
    if (args.empty()) {
×
516
        args.emplace_back("prql-expr");
×
517
        return Ok(retval);
×
518
    }
519

520
    return Ok(retval);
×
521
}
522

523
static Result<std::string, lnav::console::user_message>
524
prql_cmd_append(exec_context& ec,
×
525
                std::string cmdline,
526
                std::vector<std::string>& args)
527
{
528
    std::string retval;
×
529

530
    if (args.empty()) {
×
531
        args.emplace_back("prql-table");
×
532
        return Ok(retval);
×
533
    }
534

535
    return Ok(retval);
×
536
}
537

538
static Result<std::string, lnav::console::user_message>
539
prql_cmd_derive(exec_context& ec,
×
540
                std::string cmdline,
541
                std::vector<std::string>& args)
542
{
543
    std::string retval;
×
544

545
    if (args.empty()) {
×
546
        args.emplace_back("prql-expr");
×
547
        return Ok(retval);
×
548
    }
549

550
    return Ok(retval);
×
551
}
552

553
static Result<std::string, lnav::console::user_message>
554
prql_cmd_filter(exec_context& ec,
×
555
                std::string cmdline,
556
                std::vector<std::string>& args)
557
{
558
    std::string retval;
×
559

560
    if (args.empty()) {
×
561
        args.emplace_back("prql-expr");
×
562
        return Ok(retval);
×
563
    }
564

565
    return Ok(retval);
×
566
}
567

568
static Result<std::string, lnav::console::user_message>
569
prql_cmd_group(exec_context& ec,
×
570
               std::string cmdline,
571
               std::vector<std::string>& args)
572
{
573
    std::string retval;
×
574

575
    if (args.empty()) {
×
576
        args.emplace_back("prql-expr");
×
577
        args.emplace_back("prql-source");
×
578
        return Ok(retval);
×
579
    }
580

581
    return Ok(retval);
×
582
}
583

584
static Result<std::string, lnav::console::user_message>
585
prql_cmd_join(exec_context& ec,
×
586
              std::string cmdline,
587
              std::vector<std::string>& args)
588
{
589
    std::string retval;
×
590

591
    if (args.empty()) {
×
592
        args.emplace_back("prql-table");
×
593
        args.emplace_back("prql-expr");
×
594
        return Ok(retval);
×
595
    }
596

597
    return Ok(retval);
×
598
}
599

600
static Result<std::string, lnav::console::user_message>
601
prql_cmd_select(exec_context& ec,
×
602
                std::string cmdline,
603
                std::vector<std::string>& args)
604
{
605
    std::string retval;
×
606

607
    if (args.empty()) {
×
608
        args.emplace_back("prql-expr");
×
609
        return Ok(retval);
×
610
    }
611

612
    return Ok(retval);
×
613
}
614

615
static Result<std::string, lnav::console::user_message>
616
prql_cmd_sort(exec_context& ec,
×
617
              std::string cmdline,
618
              std::vector<std::string>& args)
619
{
620
    std::string retval;
×
621

622
    if (args.empty()) {
×
623
        args.emplace_back("prql-expr");
×
624
        return Ok(retval);
×
625
    }
626

627
    return Ok(retval);
×
628
}
629

630
static Result<std::string, lnav::console::user_message>
631
prql_cmd_take(exec_context& ec,
×
632
              std::string cmdline,
633
              std::vector<std::string>& args)
634
{
635
    std::string retval;
×
636

637
    if (args.empty()) {
×
638
        return Ok(retval);
×
639
    }
640

641
    return Ok(retval);
×
642
}
643

644
static readline_context::command_t sql_commands[] = {
645
    {
646
        ".dump",
647
        sql_cmd_dump,
648
        help_text(
649
            ".dump",
650
            "Dump one or more tables to a file as a series of SQL statements")
651
            .sql_command()
652
            .with_parameter(
653
                help_text{"path", "The path to the file to write"}.with_format(
654
                    help_parameter_format_t::HPF_LOCAL_FILENAME))
655
            .with_parameter(help_text{"table", "The name of the table to dump"}
656
                                .one_or_more())
657
            .with_tags({
658
                "io",
659
            }),
660
    },
661
    {
662
        ".save",
663
        sql_cmd_save,
664
        help_text(".save", "Save the current database to a SQLite file")
665
            .sql_command()
666
            .with_parameter(
667
                help_text{"path", "The path to the file to write"}.with_format(
668
                    help_parameter_format_t::HPF_LOCAL_FILENAME))
669
            .with_tags({
670
                "io",
671
            }),
672
    },
673
    {
674
        ".msgformats",
675
        sql_cmd_msgformats,
676
        help_text(".msgformats",
677
                  "Executes a query that will summarize the different message "
678
                  "formats found in the logs")
679
            .sql_command(),
680
    },
681
    {
682
        ".read",
683
        sql_cmd_read,
684
        help_text(".read", "Execute the SQLite statements in the given file")
685
            .sql_command()
686
            .with_parameter(
687
                help_text{"path", "The path to the file to write"}.with_format(
688
                    help_parameter_format_t::HPF_LOCAL_FILENAME))
689
            .with_tags({
690
                "io",
691
            }),
692
    },
693
    {
694
        ".schema",
695
        sql_cmd_schema,
696
        help_text(".schema",
697
                  "Switch to the SCHEMA view that contains a dump of the "
698
                  "current database schema")
699
            .sql_command()
700
            .with_parameter({"name", "The name of a table to jump to"}),
701
    },
702
    {
703
        "ATTACH",
704
        sql_cmd_generic,
705
    },
706
    {
707
        "CREATE",
708
        sql_cmd_generic,
709
    },
710
    {
711
        "DELETE",
712
        sql_cmd_generic,
713
    },
714
    {
715
        "DETACH",
716
        sql_cmd_generic,
717
    },
718
    {
719
        "DROP",
720
        sql_cmd_generic,
721
    },
722
    {
723
        "INSERT",
724
        sql_cmd_generic,
725
    },
726
    {
727
        "SELECT",
728
        sql_cmd_generic,
729
    },
730
    {
731
        "UPDATE",
732
        sql_cmd_generic,
733
    },
734
    {
735
        "WITH",
736
        sql_cmd_generic,
737
    },
738
    {
739
        "from",
740
        prql_cmd_from,
741
        help_text("from")
742
            .prql_transform()
743
            .with_tags({"prql"})
744
            .with_summary("PRQL command to specify a data source")
745
            .with_parameter({"table", "The table to use as a source"})
746
            .with_example({
747
                "To pull data from the 'http_status_codes' database table",
748
                "from http_status_codes | take 3",
749
                help_example::language::prql,
750
            })
751
            .with_example({
752
                "To use an array literal as a source",
753
                "from [{ col1=1, col2='abc' }, { col1=2, col2='def' }]",
754
                help_example::language::prql,
755
            }),
756
        prql_cmd_from_prompt,
757
        "prql-source",
758
    },
759
    {
760
        "aggregate",
761
        prql_cmd_aggregate,
762
        help_text("aggregate")
763
            .prql_transform()
764
            .with_tags({"prql"})
765
            .with_summary("PRQL transform to summarize many rows into one")
766
            .with_parameter(
767
                help_text{"expr", "The aggregate expression(s)"}.with_grouping(
768
                    "{", "}"))
769
            .with_example({
770
                "To group values into a JSON array",
771
                "from [{a=1}, {a=2}] | aggregate { arr = json.group_array a }",
772
                help_example::language::prql,
773
            }),
774
        nullptr,
775
        "prql-source",
776
        {"prql-source"},
777
    },
778
    {
779
        "append",
780
        prql_cmd_append,
781
        help_text("append")
782
            .prql_transform()
783
            .with_tags({"prql"})
784
            .with_summary("PRQL transform to concatenate tables together")
785
            .with_parameter({"table", "The table to use as a source"}),
786
        nullptr,
787
        "prql-source",
788
        {"prql-source"},
789
    },
790
    {
791
        "derive",
792
        prql_cmd_derive,
793
        help_text("derive")
794
            .prql_transform()
795
            .with_tags({"prql"})
796
            .with_summary("PRQL transform to derive one or more columns")
797
            .with_parameter(
798
                help_text{"column", "The new column"}.with_grouping("{", "}"))
799
            .with_example({
800
                "To add a column that is a multiplication of another",
801
                "from [{a=1}, {a=2}] | derive b = a * 2",
802
                help_example::language::prql,
803
            }),
804
        nullptr,
805
        "prql-source",
806
        {"prql-source"},
807
    },
808
    {
809
        "filter",
810
        prql_cmd_filter,
811
        help_text("filter")
812
            .prql_transform()
813
            .with_tags({"prql"})
814
            .with_summary("PRQL transform to pick rows based on their values")
815
            .with_parameter(
816
                {"expr", "The expression to evaluate over each row"})
817
            .with_example({
818
                "To pick rows where 'a' is greater than one",
819
                "from [{a=1}, {a=2}] | filter a > 1",
820
                help_example::language::prql,
821
            }),
822
        nullptr,
823
        "prql-source",
824
        {"prql-source"},
825
    },
826
    {
827
        "group",
828
        prql_cmd_group,
829
        help_text("group")
830
            .prql_transform()
831
            .with_tags({"prql"})
832
            .with_summary("PRQL transform to partition rows into groups")
833
            .with_parameter(
834
                help_text{"key_columns", "The columns that define the group"}
835
                    .with_grouping("{", "}"))
836
            .with_parameter(
837
                help_text{"pipeline", "The pipeline to execute over a group"}
838
                    .with_grouping("(", ")"))
839
            .with_example({
840
                "To group by log_level and count the rows in each partition",
841
                "from lnav_example_log | group { log_level } (aggregate { "
842
                "count this })",
843
                help_example::language::prql,
844
            }),
845
        nullptr,
846
        "prql-source",
847
        {"prql-source"},
848
    },
849
    {
850
        "join",
851
        prql_cmd_join,
852
        help_text("join")
853
            .prql_transform()
854
            .with_tags({"prql"})
855
            .with_summary("PRQL transform to add columns from another table")
856
            .with_parameter(help_text{"side", "Specifies which rows to include"}
857
                                .with_enum_values({
858
                                    "inner"_frag,
859
                                    "left"_frag,
860
                                    "right"_frag,
861
                                    "full"_frag,
862
                                })
863
                                .with_default_value("inner")
864
                                .optional())
865
            .with_parameter(
866
                {"table", "The other table to join with the current rows"})
867
            .with_parameter(
868
                help_text{"condition", "The condition used to join rows"}
869
                    .with_grouping("(", ")")),
870
        nullptr,
871
        "prql-source",
872
        {"prql-source"},
873
    },
874
    {
875
        "select",
876
        prql_cmd_select,
877
        help_text("select")
878
            .prql_transform()
879
            .with_tags({"prql"})
880
            .with_summary("PRQL transform to pick and compute columns")
881
            .with_parameter(
882
                help_text{"expr", "The columns to include in the result set"}
883
                    .with_grouping("{", "}"))
884
            .with_example({
885
                "To pick the 'b' column from the rows",
886
                "from [{a=1, b='abc'}, {a=2, b='def'}] | select b",
887
                help_example::language::prql,
888
            })
889
            .with_example({
890
                "To compute a new column from an input",
891
                "from [{a=1}, {a=2}] | select b = a * 2",
892
                help_example::language::prql,
893
            }),
894
        nullptr,
895
        "prql-source",
896
        {"prql-source"},
897
    },
898
    {
899
        "stats.average_of",
900
        prql_cmd_sort,
901
        help_text("stats.average_of", "Compute the average of col")
902
            .prql_function()
903
            .with_tags({"prql"})
904
            .with_parameter(help_text{"col", "The column to average"})
905
            .with_example({
906
                "To get the average of a",
907
                "from [{a=1}, {a=1}, {a=2}] | stats.average_of a",
908
                help_example::language::prql,
909
            }),
910
        nullptr,
911
        "prql-source",
912
        {"prql-source"},
913
    },
914
    {
915
        "stats.count_by",
916
        prql_cmd_sort,
917
        help_text(
918
            "stats.count_by",
919
            "Partition rows and count the number of rows in each partition")
920
            .prql_function()
921
            .with_tags({"prql"})
922
            .with_parameter(help_text{"column", "The columns to group by"}
923
                                .one_or_more()
924
                                .with_grouping("{", "}"))
925
            .with_example({
926
                "To count rows for a particular value of column 'a'",
927
                "from [{a=1}, {a=1}, {a=2}] | stats.count_by a",
928
                help_example::language::prql,
929
            }),
930
        nullptr,
931
        "prql-source",
932
        {"prql-source"},
933
    },
934
    {
935
        "stats.hist",
936
        prql_cmd_sort,
937
        help_text("stats.hist", "Count the top values per bucket of time")
938
            .prql_function()
939
            .with_tags({"prql"})
940
            .with_parameter(help_text{"col", "The column to count"})
941
            .with_parameter(help_text{"slice", "The time slice"}
942
                                .optional()
943
                                .with_default_value("'1h'"))
944
            .with_parameter(
945
                help_text{"top", "The limit on the number of values to report"}
946
                    .optional()
947
                    .with_default_value("10"))
948
            .with_example({
949
                "To chart the values of ex_procname over time",
950
                "from lnav_example_log | stats.hist ex_procname",
951
                help_example::language::prql,
952
            }),
953
        nullptr,
954
        "prql-source",
955
        {"prql-source"},
956
    },
957
    {
958
        "stats.sum_of",
959
        prql_cmd_sort,
960
        help_text("stats.sum_of", "Compute the sum of col")
961
            .prql_function()
962
            .with_tags({"prql"})
963
            .with_parameter(help_text{"col", "The column to sum"})
964
            .with_example({
965
                "To get the sum of a",
966
                "from [{a=1}, {a=1}, {a=2}] | stats.sum_of a",
967
                help_example::language::prql,
968
            }),
969
        nullptr,
970
        "prql-source",
971
        {"prql-source"},
972
    },
973
    {
974
        "stats.by",
975
        prql_cmd_sort,
976
        help_text("stats.by", "A shorthand for grouping and aggregating")
977
            .prql_function()
978
            .with_tags({"prql"})
979
            .with_parameter(help_text{"col", "The column to sum"})
980
            .with_parameter(help_text{"values", "The aggregations to perform"})
981
            .with_example({
982
                "To partition by a and get the sum of b",
983
                "from [{a=1, b=1}, {a=1, b=1}, {a=2, b=1}] | stats.by a "
984
                "{sum b}",
985
                help_example::language::prql,
986
            }),
987
        nullptr,
988
        "prql-source",
989
        {"prql-source"},
990
    },
991
    {
992
        "sort",
993
        prql_cmd_sort,
994
        help_text("sort")
995
            .prql_transform()
996
            .with_tags({"prql"})
997
            .with_summary("PRQL transform to sort rows")
998
            .with_parameter(help_text{
999
                "expr", "The values to use when ordering the result set"}
1000
                                .with_grouping("{", "}"))
1001
            .with_example({
1002
                "To sort the rows in descending order",
1003
                "from [{a=1}, {a=2}] | sort {-a}",
1004
                help_example::language::prql,
1005
            }),
1006
        nullptr,
1007
        "prql-source",
1008
        {"prql-source"},
1009
    },
1010
    {
1011
        "take",
1012
        prql_cmd_take,
1013
        help_text("take")
1014
            .prql_transform()
1015
            .with_tags({"prql"})
1016
            .with_summary("PRQL command to pick rows based on their position")
1017
            .with_parameter({"n_or_range", "The number of rows or range"})
1018
            .with_example({
1019
                "To pick the first row",
1020
                "from [{a=1}, {a=2}, {a=3}] | take 1",
1021
                help_example::language::prql,
1022
            })
1023
            .with_example({
1024
                "To pick the second and third rows",
1025
                "from [{a=1}, {a=2}, {a=3}] | take 2..3",
1026
                help_example::language::prql,
1027
            }),
1028
        nullptr,
1029
        "prql-source",
1030
        {"prql-source"},
1031
    },
1032
    {
1033
        "utils.distinct",
1034
        prql_cmd_sort,
1035
        help_text("utils.distinct",
1036
                  "A shorthand for getting distinct values of col")
1037
            .prql_function()
1038
            .with_tags({"prql"})
1039
            .with_parameter(help_text{"col", "The column to sum"})
1040
            .with_example({
1041
                "To get the distinct values of a",
1042
                "from [{a=1}, {a=1}, {a=2}] | utils.distinct a",
1043
                help_example::language::prql,
1044
            }),
1045
        nullptr,
1046
        "prql-source",
1047
        {"prql-source"},
1048
    },
1049
};
1050

1051
static readline_context::command_map_t sql_cmd_map;
1052

1053
static auto bound_sql_cmd_map
1054
    = injector::bind<readline_context::command_map_t,
1055
                     sql_cmd_map_tag>::to_instance(+[]() {
1,124✔
1056
          for (auto& cmd : sql_commands) {
34,844✔
1057
              sql_cmd_map[cmd.c_name] = &cmd;
67,440✔
1058
              if (cmd.c_help.ht_name) {
33,720✔
1059
                  cmd.c_help.index_tags();
33,720✔
1060
              }
1061
          }
1062

1063
          return &sql_cmd_map;
1,124✔
1064
      });
1065

1066
namespace injector {
1067
template<>
1068
void
1069
force_linking(sql_cmd_map_tag anno)
19✔
1070
{
1071
}
19✔
1072
}  // namespace injector
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