diff options
Diffstat (limited to '')
-rw-r--r-- | src/views_vtab.cc | 1074 |
1 files changed, 1074 insertions, 0 deletions
diff --git a/src/views_vtab.cc b/src/views_vtab.cc new file mode 100644 index 0000000..cf0a7cb --- /dev/null +++ b/src/views_vtab.cc @@ -0,0 +1,1074 @@ +/** + * Copyright (c) 2015, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <cstring> + +#include "views_vtab.hh" + +#include <unistd.h> + +#include "base/injector.bind.hh" +#include "base/itertools.hh" +#include "base/lnav_log.hh" +#include "base/opt_util.hh" +#include "config.h" +#include "lnav.hh" +#include "lnav_util.hh" +#include "sql_util.hh" +#include "view_curses.hh" +#include "vtab_module_json.hh" +#include "yajlpp/yajlpp_def.hh" + +template<> +struct from_sqlite<lnav_view_t> { + inline lnav_view_t operator()(int argc, sqlite3_value** val, int argi) + { + const char* view_name = (const char*) sqlite3_value_text(val[argi]); + auto view_index_opt = view_from_string(view_name); + + if (!view_index_opt) { + throw from_sqlite_conversion_error("lnav view name", argi); + } + + return view_index_opt.value(); + } +}; + +template<> +struct from_sqlite<text_filter::type_t> { + inline text_filter::type_t operator()(int argc, + sqlite3_value** val, + int argi) + { + const char* type_name = (const char*) sqlite3_value_text(val[argi]); + + if (strcasecmp(type_name, "in") == 0) { + return text_filter::INCLUDE; + } + if (strcasecmp(type_name, "out") == 0) { + return text_filter::EXCLUDE; + } + + throw from_sqlite_conversion_error("value of 'in' or 'out'", argi); + } +}; + +template<> +struct from_sqlite<filter_lang_t> { + inline filter_lang_t operator()(int argc, sqlite3_value** val, int argi) + { + const char* type_name = (const char*) sqlite3_value_text(val[argi]); + + if (strcasecmp(type_name, "regex") == 0) { + return filter_lang_t::REGEX; + } + if (strcasecmp(type_name, "sql") == 0) { + return filter_lang_t::SQL; + } + + throw from_sqlite_conversion_error("value of 'regex' or 'sql'", argi); + } +}; + +template<> +struct from_sqlite<std::shared_ptr<lnav::pcre2pp::code>> { + inline std::shared_ptr<lnav::pcre2pp::code> operator()(int argc, + sqlite3_value** val, + int argi) + { + const char* pattern = (const char*) sqlite3_value_text(val[argi]); + + if (pattern == nullptr || pattern[0] == '\0') { + throw sqlite_func_error("Expecting a non-empty pattern value"); + } + + auto compile_res = lnav::pcre2pp::code::from( + string_fragment::from_c_str(pattern), PCRE2_CASELESS); + + if (compile_res.isErr()) { + auto ce = compile_res.unwrapErr(); + throw sqlite_func_error( + "Invalid regular expression for pattern: {} at offset {}", + ce.get_message().c_str(), + ce.ce_offset); + } + + return compile_res.unwrap().to_shared(); + } +}; + +static const typed_json_path_container<breadcrumb::possibility> + breadcrumb_possibility_handlers = { + yajlpp::property_handler("display_value") + .for_field(&breadcrumb::possibility::p_display_value, + &attr_line_t::al_string), +}; + +struct resolved_crumb { + resolved_crumb() = default; + + resolved_crumb(std::string display_value, + std::string search_placeholder, + std::vector<breadcrumb::possibility> possibilities) + : rc_display_value(std::move(display_value)), + rc_search_placeholder(std::move(search_placeholder)), + rc_possibilities(std::move(possibilities)) + { + } + + std::string rc_display_value; + std::string rc_search_placeholder; + std::vector<breadcrumb::possibility> rc_possibilities; +}; + +static const typed_json_path_container<resolved_crumb> breadcrumb_crumb_handlers + = { + yajlpp::property_handler("display_value") + .for_field(&resolved_crumb::rc_display_value), + yajlpp::property_handler("search_placeholder") + .for_field(&resolved_crumb::rc_search_placeholder), + yajlpp::property_handler("possibilities#") + .for_field(&resolved_crumb::rc_possibilities) + .with_children(breadcrumb_possibility_handlers), +}; + +struct top_line_meta { + nonstd::optional<std::string> tlm_time; + nonstd::optional<std::string> tlm_file; + nonstd::optional<std::string> tlm_anchor; + std::vector<resolved_crumb> tlm_crumbs; +}; + +static const typed_json_path_container<top_line_meta> top_line_meta_handlers = { + yajlpp::property_handler("time").for_field(&top_line_meta::tlm_time), + yajlpp::property_handler("file").for_field(&top_line_meta::tlm_file), + yajlpp::property_handler("anchor").for_field(&top_line_meta::tlm_anchor), + yajlpp::property_handler("breadcrumbs#") + .for_field(&top_line_meta::tlm_crumbs) + .with_children(breadcrumb_crumb_handlers), +}; + +struct lnav_views : public tvt_iterator_cursor<lnav_views> { + static constexpr const char* NAME = "lnav_views"; + static constexpr const char* CREATE_STMT = R"( +-- Access lnav's views through this table. +CREATE TABLE lnav_views ( + name TEXT PRIMARY KEY, -- The name of the view. + top INTEGER, -- The number of the line at the top of the view, starting from zero. + left INTEGER, -- The left position of the viewport. + height INTEGER, -- The height of the viewport. + inner_height INTEGER, -- The number of lines in the view. + top_time DATETIME, -- The time of the top line in the view, if the content is time-based. + top_file TEXT, -- The file the top line is from. + paused INTEGER, -- Indicates if the view is paused and will not load new data. + search TEXT, -- The text to search for in the view. + filtering INTEGER, -- Indicates if the view is applying filters. + top_meta TEXT -- A JSON object that contains metadata related to the top line in the view. +); +)"; + + using iterator = textview_curses*; + + iterator begin() { return std::begin(lnav_data.ld_views); } + + iterator end() { return std::end(lnav_data.ld_views); } + + int get_column(cursor& vc, sqlite3_context* ctx, int col) + { + lnav_view_t view_index = (lnav_view_t) std::distance( + std::begin(lnav_data.ld_views), vc.iter); + textview_curses& tc = *vc.iter; + unsigned long width; + vis_line_t height; + + tc.get_dimensions(height, width); + switch (col) { + case 0: + sqlite3_result_text( + ctx, lnav_view_strings[view_index], -1, SQLITE_STATIC); + break; + case 1: + sqlite3_result_int(ctx, (int) tc.get_top()); + break; + case 2: + sqlite3_result_int(ctx, tc.get_left()); + break; + case 3: + sqlite3_result_int(ctx, height); + break; + case 4: + sqlite3_result_int(ctx, tc.get_inner_height()); + break; + case 5: { + auto* time_source + = dynamic_cast<text_time_translator*>(tc.get_sub_source()); + + if (time_source != nullptr && tc.get_inner_height() > 0) { + auto top_time_opt = time_source->time_for_row(tc.get_top()); + + if (top_time_opt) { + char timestamp[64]; + + sql_strftime(timestamp, + sizeof(timestamp), + top_time_opt.value(), + ' '); + sqlite3_result_text( + ctx, timestamp, -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_null(ctx); + } + } else { + sqlite3_result_null(ctx); + } + break; + } + case 6: { + to_sqlite(ctx, tc.map_top_row([](const auto& al) { + return get_string_attr(al.get_attrs(), logline::L_FILE) | + [](const auto wrapper) { + auto lf = wrapper.get(); + + return nonstd::make_optional(lf->get_filename()); + }; + })); + break; + } + case 7: + sqlite3_result_int(ctx, tc.is_paused()); + break; + case 8: + to_sqlite(ctx, tc.get_current_search()); + break; + case 9: { + auto* tss = tc.get_sub_source(); + + if (tss != nullptr && tss->tss_supports_filtering) { + sqlite3_result_int(ctx, tss->tss_apply_filters); + } else { + sqlite3_result_int(ctx, 0); + } + break; + } + case 10: { + static const size_t MAX_POSSIBILITIES = 128; + + auto* tss = tc.get_sub_source(); + + if (tss != nullptr && tss->text_line_count() > 0) { + auto* time_source = dynamic_cast<text_time_translator*>( + tc.get_sub_source()); + auto* ta = dynamic_cast<text_anchors*>(tc.get_sub_source()); + std::vector<breadcrumb::crumb> crumbs; + + tss->text_crumbs_for_line(tc.get_top(), crumbs); + + top_line_meta tlm; + if (time_source != nullptr) { + auto top_time_opt + = time_source->time_for_row(tc.get_top()); + + if (top_time_opt) { + char timestamp[64]; + + sql_strftime(timestamp, + sizeof(timestamp), + top_time_opt.value(), + ' '); + tlm.tlm_time = timestamp; + } + } + if (ta != nullptr) { + tlm.tlm_anchor = ta->anchor_for_row(tc.get_top()); + } + tlm.tlm_file = tc.map_top_row([](const auto& al) { + return get_string_attr(al.get_attrs(), logline::L_FILE) + | [](const auto wrapper) { + auto lf = wrapper.get(); + + return nonstd::make_optional( + lf->get_filename()); + }; + }); + for (const auto& crumb : crumbs) { + auto poss = crumb.c_possibility_provider(); + if (poss.size() > MAX_POSSIBILITIES) { + poss.resize(MAX_POSSIBILITIES); + } + tlm.tlm_crumbs.emplace_back( + crumb.c_display_value.get_string(), + crumb.c_search_placeholder, + std::move(poss)); + } + to_sqlite(ctx, top_line_meta_handlers.to_json_string(tlm)); + } else { + sqlite3_result_null(ctx); + } + break; + } + } + + return SQLITE_OK; + } + + int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid) + { + tab->zErrMsg = sqlite3_mprintf( + "Rows cannot be deleted from the lnav_views table"); + return SQLITE_ERROR; + } + + int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out) + { + tab->zErrMsg = sqlite3_mprintf( + "Rows cannot be inserted into the lnav_views table"); + return SQLITE_ERROR; + } + + int update_row(sqlite3_vtab* tab, + sqlite3_int64& index, + const char* name, + int64_t top_row, + int64_t left, + int64_t height, + int64_t inner_height, + const char* top_time, + const char* top_file, + bool is_paused, + const char* search, + bool do_filtering, + const char* top_meta) + { + auto& tc = lnav_data.ld_views[index]; + auto* time_source + = dynamic_cast<text_time_translator*>(tc.get_sub_source()); + + if (tc.get_top() != top_row) { + tc.set_top(vis_line_t(top_row)); + } else if (top_time != nullptr && time_source != nullptr) { + date_time_scanner dts; + struct timeval tv; + + if (dts.convert_to_timeval(top_time, -1, nullptr, tv)) { + auto last_time_opt = time_source->time_for_row(tc.get_top()); + + if (last_time_opt) { + auto last_time = last_time_opt.value(); + if (tv != last_time) { + time_source->row_for_time(tv) | + [&tc](auto row) { tc.set_top(row); }; + } + } + } else { + tab->zErrMsg = sqlite3_mprintf("Invalid time: %s", top_time); + return SQLITE_ERROR; + } + } + if (top_meta != nullptr) { + static const intern_string_t SQL_SRC + = intern_string::lookup("top_meta"); + + auto parse_res = top_line_meta_handlers.parser_for(SQL_SRC).of( + string_fragment::from_c_str(top_meta)); + if (parse_res.isErr()) { + auto errmsg = parse_res.unwrapErr(); + tab->zErrMsg = sqlite3_mprintf( + "Invalid top_meta: %s", + errmsg[0].to_attr_line().get_string().c_str()); + return SQLITE_ERROR; + } + + auto tlm = parse_res.unwrap(); + + if (index == LNV_TEXT && tlm.tlm_file) { + if (!lnav_data.ld_text_source.to_front(tlm.tlm_file.value())) { + auto errmsg = parse_res.unwrapErr(); + tab->zErrMsg = sqlite3_mprintf("unknown top_meta.file: %s", + tlm.tlm_file->c_str()); + return SQLITE_ERROR; + } + } + + auto* ta = dynamic_cast<text_anchors*>(tc.get_sub_source()); + if (ta != nullptr && tlm.tlm_anchor + && !tlm.tlm_anchor.value().empty()) + { + auto req_anchor = tlm.tlm_anchor.value(); + auto req_anchor_top = ta->row_for_anchor(req_anchor); + if (req_anchor_top) { + auto curr_anchor = ta->anchor_for_row(tc.get_top()); + + if (!curr_anchor || curr_anchor.value() != req_anchor) { + tc.set_top(req_anchor_top.value()); + } + } else { + tab->zErrMsg = sqlite3_mprintf( + "unknown top_meta.anchor: %s", req_anchor.c_str()); + return SQLITE_ERROR; + } + } + } + tc.set_left(left); + tc.set_paused(is_paused); + tc.execute_search(search); + auto tss = tc.get_sub_source(); + if (tss != nullptr && tss->tss_supports_filtering + && tss->tss_apply_filters != do_filtering) + { + tss->tss_apply_filters = do_filtering; + tss->text_filters_changed(); + } + + return SQLITE_OK; + }; +}; + +struct lnav_view_stack : public tvt_iterator_cursor<lnav_view_stack> { + using iterator = std::vector<textview_curses*>::iterator; + + static constexpr const char* NAME = "lnav_view_stack"; + static constexpr const char* CREATE_STMT = R"( +-- Access lnav's view stack through this table. +CREATE TABLE lnav_view_stack ( + name TEXT +); +)"; + + iterator begin() { return lnav_data.ld_view_stack.begin(); } + + iterator end() { return lnav_data.ld_view_stack.end(); } + + int get_column(cursor& vc, sqlite3_context* ctx, int col) + { + textview_curses* tc = *vc.iter; + auto view = lnav_view_t(tc - lnav_data.ld_views); + + switch (col) { + case 0: + sqlite3_result_text( + ctx, lnav_view_strings[view], -1, SQLITE_STATIC); + break; + } + + return SQLITE_OK; + } + + int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid) + { + if ((size_t) rowid != lnav_data.ld_view_stack.size() - 1) { + tab->zErrMsg = sqlite3_mprintf( + "Only the top view in the stack can be deleted"); + return SQLITE_ERROR; + } + + lnav_data.ld_last_view = *lnav_data.ld_view_stack.top(); + lnav_data.ld_view_stack.pop_back(); + return SQLITE_OK; + } + + int insert_row(sqlite3_vtab* tab, + sqlite3_int64& rowid_out, + lnav_view_t view_index) + { + textview_curses* tc = &lnav_data.ld_views[view_index]; + + ensure_view(tc); + rowid_out = lnav_data.ld_view_stack.size() - 1; + + return SQLITE_OK; + } + + int update_row(sqlite3_vtab* tab, sqlite3_int64& index) + { + tab->zErrMsg + = sqlite3_mprintf("The lnav_view_stack table cannot be updated"); + return SQLITE_ERROR; + } +}; + +struct lnav_view_filter_base { + struct iterator { + using difference_type = int; + using value_type = text_filter; + using pointer = text_filter*; + using reference = text_filter&; + using iterator_category = std::forward_iterator_tag; + + lnav_view_t i_view_index; + int i_filter_index; + + iterator(lnav_view_t view = LNV_LOG, int filter = -1) + : i_view_index(view), i_filter_index(filter) + { + } + + iterator& operator++() + { + while (this->i_view_index < LNV__MAX) { + textview_curses& tc = lnav_data.ld_views[this->i_view_index]; + text_sub_source* tss = tc.get_sub_source(); + + if (tss == nullptr) { + this->i_view_index = lnav_view_t(this->i_view_index + 1); + continue; + } + + filter_stack& fs = tss->get_filters(); + + this->i_filter_index += 1; + if (this->i_filter_index >= (ssize_t) fs.size()) { + this->i_filter_index = -1; + this->i_view_index = lnav_view_t(this->i_view_index + 1); + } else { + break; + } + } + + return *this; + } + + bool operator==(const iterator& other) const + { + return this->i_view_index == other.i_view_index + && this->i_filter_index == other.i_filter_index; + } + + bool operator!=(const iterator& other) const + { + return !(*this == other); + } + }; + + iterator begin() + { + iterator retval = iterator(); + + return ++retval; + } + + iterator end() { return {LNV__MAX, -1}; } + + sqlite_int64 get_rowid(iterator iter) + { + textview_curses& tc = lnav_data.ld_views[iter.i_view_index]; + text_sub_source* tss = tc.get_sub_source(); + filter_stack& fs = tss->get_filters(); + auto& tf = *(fs.begin() + iter.i_filter_index); + + sqlite_int64 retval = iter.i_view_index; + + retval = retval << 32; + retval = retval | tf->get_index(); + + return retval; + } +}; + +struct lnav_view_filters + : public tvt_iterator_cursor<lnav_view_filters> + , public lnav_view_filter_base { + static constexpr const char* NAME = "lnav_view_filters"; + static constexpr const char* CREATE_STMT = R"( +-- Access lnav's filters through this table. +CREATE TABLE lnav_view_filters ( + view_name TEXT, -- The name of the view. + filter_id INTEGER DEFAULT 0, -- The filter identifier. + enabled INTEGER DEFAULT 1, -- Indicates if the filter is enabled/disabled. + type TEXT DEFAULT 'out', -- The type of filter (i.e. in/out). + language TEXT DEFAULT 'regex', -- The filter language. + pattern TEXT -- The filter pattern. +); +)"; + + int get_column(cursor& vc, sqlite3_context* ctx, int col) + { + textview_curses& tc = lnav_data.ld_views[vc.iter.i_view_index]; + text_sub_source* tss = tc.get_sub_source(); + filter_stack& fs = tss->get_filters(); + auto tf = *(fs.begin() + vc.iter.i_filter_index); + + switch (col) { + case 0: + sqlite3_result_text(ctx, + lnav_view_strings[vc.iter.i_view_index], + -1, + SQLITE_STATIC); + break; + case 1: + to_sqlite(ctx, tf->get_index()); + break; + case 2: + sqlite3_result_int(ctx, tf->is_enabled()); + break; + case 3: + switch (tf->get_type()) { + case text_filter::INCLUDE: + sqlite3_result_text(ctx, "in", 2, SQLITE_STATIC); + break; + case text_filter::EXCLUDE: + sqlite3_result_text(ctx, "out", 3, SQLITE_STATIC); + break; + default: + ensure(0); + } + break; + case 4: + switch (tf->get_lang()) { + case filter_lang_t::REGEX: + sqlite3_result_text(ctx, "regex", 5, SQLITE_STATIC); + break; + case filter_lang_t::SQL: + sqlite3_result_text(ctx, "sql", 3, SQLITE_STATIC); + break; + default: + ensure(0); + } + break; + case 5: + sqlite3_result_text( + ctx, tf->get_id().c_str(), -1, SQLITE_TRANSIENT); + break; + } + + return SQLITE_OK; + } + + int insert_row(sqlite3_vtab* tab, + sqlite3_int64& rowid_out, + lnav_view_t view_index, + nonstd::optional<int64_t> _filter_id, + nonstd::optional<bool> enabled, + nonstd::optional<text_filter::type_t> type, + nonstd::optional<filter_lang_t> lang, + sqlite3_value* pattern_str) + { + auto* mod_vt = (vtab_module<lnav_view_filters>::vtab*) tab; + auto& tc = lnav_data.ld_views[view_index]; + auto* tss = tc.get_sub_source(); + auto& fs = tss->get_filters(); + auto filter_index + = lang.value_or(filter_lang_t::REGEX) == filter_lang_t::REGEX + ? fs.next_index() + : nonstd::make_optional(size_t{0}); + if (!filter_index) { + throw sqlite_func_error("Too many filters"); + } + auto conflict_mode = sqlite3_vtab_on_conflict(mod_vt->v_db); + std::shared_ptr<text_filter> tf; + switch (lang.value_or(filter_lang_t::REGEX)) { + case filter_lang_t::REGEX: { + auto pattern + = from_sqlite<std::shared_ptr<lnav::pcre2pp::code>>()( + 1, &pattern_str, 0); + auto pf = std::make_shared<pcre_filter>( + type.value_or(text_filter::type_t::EXCLUDE), + pattern->get_pattern(), + *filter_index, + pattern); + auto new_cmd = pf->to_command(); + for (auto& filter : fs) { + if (filter->to_command() == new_cmd) { + switch (conflict_mode) { + case SQLITE_FAIL: + case SQLITE_ABORT: + tab->zErrMsg = sqlite3_mprintf( + "filter already exists -- :%s", + new_cmd.c_str()); + return conflict_mode; + case SQLITE_IGNORE: + return SQLITE_OK; + case SQLITE_REPLACE: + if (filter->is_enabled() != pf->is_enabled()) { + filter->set_enabled(pf->is_enabled()); + tss->text_filters_changed(); + tc.set_needs_update(); + } + return SQLITE_OK; + default: + break; + } + } + } + fs.add_filter(pf); + tf = pf; + break; + } + case filter_lang_t::SQL: { + if (view_index != LNV_LOG) { + throw sqlite_func_error( + "SQL filters are only supported in the log view"); + } + if (lnav_data.ld_log_source.get_sql_filter_text() != "") { + switch (conflict_mode) { + case SQLITE_FAIL: + case SQLITE_ABORT: + tab->zErrMsg = sqlite3_mprintf( + "A SQL expression filter already exists"); + return conflict_mode; + case SQLITE_IGNORE: + return SQLITE_OK; + default: + break; + } + } + auto clause = from_sqlite<std::string>()(1, &pattern_str, 0); + auto expr + = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), clause); + auto_mem<sqlite3_stmt> stmt(sqlite3_finalize); +#ifdef SQLITE_PREPARE_PERSISTENT + auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(), + expr.c_str(), + expr.size(), + SQLITE_PREPARE_PERSISTENT, + stmt.out(), + nullptr); +#else + auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(), + expr.c_str(), + expr.size(), + stmt.out(), + nullptr); +#endif + if (retcode != SQLITE_OK) { + const char* errmsg = sqlite3_errmsg(lnav_data.ld_db); + + throw sqlite_func_error("Invalid SQL: {}", errmsg); + } + auto set_res = lnav_data.ld_log_source.set_sql_filter( + clause, stmt.release()); + if (set_res.isErr()) { + tab->zErrMsg = sqlite3_mprintf( + "%s%s", + sqlitepp::ERROR_PREFIX, + lnav::to_json(set_res.unwrapErr()).c_str()); + return SQLITE_ERROR; + } + tf = lnav_data.ld_log_source.get_sql_filter().value(); + break; + } + default: + ensure(0); + } + if (!enabled.value_or(true)) { + tf->disable(); + } + tss->text_filters_changed(); + tc.set_needs_update(); + + return SQLITE_OK; + } + + int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid) + { + auto view_index = lnav_view_t(rowid >> 32); + size_t filter_index = rowid & 0xffffffffLL; + textview_curses& tc = lnav_data.ld_views[view_index]; + text_sub_source* tss = tc.get_sub_source(); + filter_stack& fs = tss->get_filters(); + + for (const auto& iter : fs) { + if (iter->get_index() == filter_index) { + fs.delete_filter(iter->get_id()); + tss->text_filters_changed(); + break; + } + } + tc.set_needs_update(); + + return SQLITE_OK; + } + + int update_row(sqlite3_vtab* tab, + sqlite3_int64& rowid, + lnav_view_t new_view_index, + int64_t new_filter_id, + bool enabled, + text_filter::type_t type, + filter_lang_t lang, + sqlite3_value* pattern_val) + { + auto* mod_vt = (vtab_module<lnav_view_filters>::vtab*) tab; + auto view_index = lnav_view_t(rowid >> 32); + auto filter_index = rowid & 0xffffffffLL; + auto& tc = lnav_data.ld_views[view_index]; + auto* tss = tc.get_sub_source(); + auto& fs = tss->get_filters(); + auto iter = fs.begin(); + for (; iter != fs.end(); ++iter) { + if ((*iter)->get_index() == (size_t) filter_index) { + break; + } + } + + auto tf = *iter; + + if (new_view_index != view_index) { + tab->zErrMsg + = sqlite3_mprintf("The view for a filter cannot be changed"); + return SQLITE_ERROR; + } + + if (lang == filter_lang_t::SQL && tf->get_index() == 0) { + if (view_index != LNV_LOG) { + throw sqlite_func_error( + "SQL filters are only supported in the log view"); + } + auto clause = from_sqlite<std::string>()(1, &pattern_val, 0); + auto expr = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), clause); + auto_mem<sqlite3_stmt> stmt(sqlite3_finalize); +#ifdef SQLITE_PREPARE_PERSISTENT + auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(), + expr.c_str(), + expr.size(), + SQLITE_PREPARE_PERSISTENT, + stmt.out(), + nullptr); +#else + auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(), + expr.c_str(), + expr.size(), + stmt.out(), + nullptr); +#endif + if (retcode != SQLITE_OK) { + const char* errmsg = sqlite3_errmsg(lnav_data.ld_db); + + throw sqlite_func_error("Invalid SQL: {}", errmsg); + } + auto set_res = lnav_data.ld_log_source.set_sql_filter( + clause, stmt.release()); + if (set_res.isErr()) { + tab->zErrMsg = sqlite3_mprintf( + "%s%s", + sqlitepp::ERROR_PREFIX, + lnav::to_json(set_res.unwrapErr()).c_str()); + return SQLITE_ERROR; + } + *iter = lnav_data.ld_log_source.get_sql_filter().value(); + } else { + tf->lf_deleted = true; + tss->text_filters_changed(); + + auto pattern = from_sqlite<std::shared_ptr<lnav::pcre2pp::code>>()( + 1, &pattern_val, 0); + auto pf = std::make_shared<pcre_filter>( + type, pattern->get_pattern(), tf->get_index(), pattern); + auto conflict_mode = sqlite3_vtab_on_conflict(mod_vt->v_db); + auto new_cmd = pf->to_command(); + for (auto& filter : fs) { + if (filter->to_command() == new_cmd) { + switch (conflict_mode) { + case SQLITE_FAIL: + case SQLITE_ABORT: + tab->zErrMsg = sqlite3_mprintf( + "filter already exists -- :%s", + new_cmd.c_str()); + return conflict_mode; + case SQLITE_IGNORE: + return SQLITE_OK; + case SQLITE_REPLACE: + if (filter->is_enabled() != pf->is_enabled()) { + filter->set_enabled(pf->is_enabled()); + tss->text_filters_changed(); + tc.set_needs_update(); + } + return SQLITE_OK; + default: + break; + } + } + } + *iter = pf; + } + if (!enabled) { + (*iter)->disable(); + } + tss->text_filters_changed(); + tc.set_needs_update(); + + return SQLITE_OK; + } +}; + +struct lnav_view_filter_stats + : public tvt_iterator_cursor<lnav_view_filter_stats> + , public lnav_view_filter_base { + static constexpr const char* NAME = "lnav_view_filter_stats"; + static constexpr const char* CREATE_STMT = R"( +-- Access statistics for filters through this table. +CREATE TABLE lnav_view_filter_stats ( + view_name TEXT, -- The name of the view. + filter_id INTEGER, -- The filter identifier. + hits INTEGER -- The number of lines that matched this filter. +); +)"; + + int get_column(cursor& vc, sqlite3_context* ctx, int col) + { + textview_curses& tc = lnav_data.ld_views[vc.iter.i_view_index]; + text_sub_source* tss = tc.get_sub_source(); + filter_stack& fs = tss->get_filters(); + auto tf = *(fs.begin() + vc.iter.i_filter_index); + + switch (col) { + case 0: + sqlite3_result_text(ctx, + lnav_view_strings[vc.iter.i_view_index], + -1, + SQLITE_STATIC); + break; + case 1: + to_sqlite(ctx, tf->get_index()); + break; + case 2: + to_sqlite(ctx, tss->get_filtered_count_for(tf->get_index())); + break; + } + + return SQLITE_OK; + } +}; + +struct lnav_view_files : public tvt_iterator_cursor<lnav_view_files> { + static constexpr const char* NAME = "lnav_view_files"; + static constexpr const char* CREATE_STMT = R"( +-- +CREATE TABLE lnav_view_files ( + view_name TEXT, -- The name of the view. + filepath TEXT, -- The path to the file. + visible INTEGER -- Indicates whether or not the file is shown. +); +)"; + + using iterator = logfile_sub_source::iterator; + + struct cursor : public tvt_iterator_cursor<lnav_view_files>::cursor { + explicit cursor(sqlite3_vtab* vt) + : tvt_iterator_cursor<lnav_view_files>::cursor(vt) + { + } + + int next() + { + if (this->iter != get_handler().end()) { + do { + ++this->iter; + } while (this->iter != get_handler().end() + && (*this->iter)->get_file_ptr() == nullptr); + } + + return SQLITE_OK; + } + }; + + iterator begin() { return lnav_data.ld_log_source.begin(); } + + iterator end() { return lnav_data.ld_log_source.end(); } + + int get_column(cursor& vc, sqlite3_context* ctx, int col) + { + auto& ld = *vc.iter; + + switch (col) { + case 0: + sqlite3_result_text( + ctx, lnav_view_strings[LNV_LOG], -1, SQLITE_STATIC); + break; + case 1: + to_sqlite(ctx, + ld->ld_filter_state.lfo_filter_state.tfs_logfile + ->get_filename()); + break; + case 2: + to_sqlite(ctx, ld->ld_visible); + break; + } + + return SQLITE_OK; + } + + int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid) + { + tab->zErrMsg = sqlite3_mprintf( + "Rows cannot be deleted from the lnav_view_files table"); + return SQLITE_ERROR; + } + + int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out) + { + tab->zErrMsg = sqlite3_mprintf( + "Rows cannot be inserted into the lnav_view_files table"); + return SQLITE_ERROR; + } + + int update_row(sqlite3_vtab* tab, + sqlite3_int64& rowid, + const char* view_name, + const char* file_path, + bool visible) + { + auto& lss = lnav_data.ld_log_source; + auto iter = this->begin(); + + std::advance(iter, rowid); + + auto& ld = *iter; + if (ld->ld_visible != visible) { + ld->set_visibility(visible); + lss.text_filters_changed(); + } + + return SQLITE_OK; + } +}; + +static const char* CREATE_FILTER_VIEW = R"( +CREATE VIEW lnav_view_filters_and_stats AS + SELECT * FROM lnav_view_filters LEFT NATURAL JOIN lnav_view_filter_stats +)"; + +static auto a = injector::bind_multiple<vtab_module_base>() + .add<vtab_module<lnav_views>>() + .add<vtab_module<lnav_view_stack>>() + .add<vtab_module<lnav_view_filters>>() + .add<vtab_module<tvt_no_update<lnav_view_filter_stats>>>() + .add<vtab_module<lnav_view_files>>(); + +int +register_views_vtab(sqlite3* db) +{ + auto_mem<char> errmsg(sqlite3_free); + if (sqlite3_exec(db, CREATE_FILTER_VIEW, nullptr, nullptr, errmsg.out()) + != SQLITE_OK) + { + log_error("Unable to create filter view: %s", errmsg.in()); + } + + return 0; +} |