backend/wbpublic/sqlide/recordset_be.cpp (1,486 lines of code) (raw):

/* * Copyright (c) 2007, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, as * designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "sqlide_generics_private.h" #include "recordset_be.h" #include "recordset_data_storage.h" #include "grt.h" #include "cppdbc.h" #include "grtui/binary_data_editor.h" #include "mforms/menubar.h" #include "mforms/toolbar.h" #include "mforms/utilities.h" #include "mforms/filechooser.h" #include "base/log.h" #include "base/string_utilities.h" #include "base/boost_smart_ptr_helpers.h" #include "sqlite/command.hpp" #include <fstream> #include <sstream> #include "grt/spatial_handler.h" #include "recordset_text_storage.h" DEFAULT_LOG_DOMAIN("Recordset") using namespace bec; using namespace base; #define CATCH_AND_DISPATCH_EXCEPTION(rethrow, context) \ catch (sql::SQLException & e) { \ rethrow ? throw \ : task->send_msg(grt::ErrorMsg, strfmt("Error Code: %i\n%s", e.getErrorCode(), e.what()), context); \ } \ catch (sqlite::database_exception & e) { \ rethrow ? throw : task->send_msg(grt::ErrorMsg, e.what(), context); \ } \ catch (std::exception & e) { \ rethrow ? throw : task->send_msg(grt::ErrorMsg, e.what(), context); \ } const std::string ERRMSG_PENDING_CHANGES = _("There are pending changes. Please commit or rollback first."); std::string Recordset::_add_change_record_statement = "insert into `changes` (`record`, `action`, `column`) values (?, ?, ?)"; Recordset::ClientData::~ClientData() { } Recordset::Ref Recordset::create() { Ref instance(new Recordset()); return instance; } Recordset::Ref Recordset::create(GrtThreadedTask::Ref parent_task) { Ref instance(new Recordset(parent_task)); return instance; } static gint next_id = 0; Recordset::Recordset() : VarGridModel(), _preserveRowFilters(false), _inserts_editor(false), task(GrtThreadedTask::create()) { _toolbar = NULL; _client_data = NULL; _context_menu = 0; _id = g_atomic_int_get(&next_id); g_atomic_int_inc(&next_id); task->desc("Recordset task"); task->send_task_res_msg(false); apply_changes_cb = [this]() { apply_changes_(); }; register_default_actions(); reset(); } Recordset::Recordset(GrtThreadedTask::Ref parent_task) : VarGridModel(), _inserts_editor(false), task(GrtThreadedTask::create(parent_task)) { _toolbar = NULL; _client_data = NULL; _context_menu = 0; _id = g_atomic_int_get(&next_id); g_atomic_int_inc(&next_id); task->send_task_res_msg(false); apply_changes_cb = [this]() { apply_changes_(); }; register_default_actions(); reset(); } Recordset::~Recordset() { // recordset can't be freed before all calls planned from this class in main thread are finished bec::GRTManager::get()->get_dispatcher()->flush_pending_callbacks(); delete _client_data; delete _context_menu; } bool Recordset::reset(Recordset_data_storage::Ptr data_storage_ptr, bool rethrow) { base::RecMutexLock data_mutex WB_UNUSED(_data_mutex); VarGridModel::reset(); std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); bool res = false; _aux_column_count = 0; _rowid_column = 0; _real_row_count = 0; _min_new_rowid = 0; _next_new_rowid = 0; _sort_columns.clear(); _column_filter_expr_map.clear(); _data_search_string.clear(); RETAIN_WEAK_PTR(Recordset_data_storage, data_storage_ptr, data_storage) if (data_storage) { try { data_storage->do_unserialize(this, data_swap_db.get()); rebuild_data_index(data_swap_db.get(), false, false); _column_count = _column_names.size(); _aux_column_count = data_storage->aux_column_count(); // add aux `id` column required by 2-level caching ++_aux_column_count; ++_column_count; _rowid_column = _column_count - 1; _column_names.push_back("id"); _column_types.push_back(int()); _real_column_types.push_back(int()); _column_flags.push_back(0); { sqlite::query q(*data_swap_db, "select coalesce(max(id)+1, 0) from `data`"); if (q.emit()) { std::shared_ptr<sqlite::result> rs = BoostHelper::convertPointer(q.get_result()); _min_new_rowid = rs->get_int(0); } else { _min_new_rowid = 0; } _next_new_rowid = _min_new_rowid; } recalc_row_count(data_swap_db.get()); _readonly = data_storage->readonly(); _readonly_reason = data_storage->readonly_reason(); res = true; } CATCH_AND_DISPATCH_EXCEPTION(rethrow, "Reset recordset") } // Don't try to refresh the (non-existing) GUI when running unit tests. // The record set is freed before this callback is triggered. if (!grt::GRT::get()->testing()) { bec::GRTManager::get()->get_dispatcher()->call_from_main_thread<void>( [this, data_swap_db]() { // We need to reapply filters once everything is loaded. if (_preserveRowFilters) { if (_toolbar != nullptr) { auto item = _toolbar->find_item("Search Field"); if (item != nullptr) { _data_search_string = item->get_text(); rebuild_data_index(data_swap_db.get(), true, false); } } } else { // Otherwise, we clear up toolbar. if (_toolbar != nullptr) { auto item = _toolbar->find_item("Search Field"); if (item != nullptr) item->set_text(""); } } }, false, false); } // Don't use refresh() to send update requests for the UI. It's regularly called from a background thread. // Instead the caller should schedule refresh calls (and coalesce them). return res; } void Recordset::reset() { reset(false); } bool Recordset::reset(bool rethrow) { return reset(_data_storage, rethrow); } bool Recordset::can_close() { return can_close(true); } bool Recordset::can_close(bool interactive) { bool res = !has_pending_changes(); if (!res && interactive) { int r = mforms::Utilities::show_warning( _("Close Recordset"), strfmt(_("There are unsaved changes to the recordset data: %s. Do you want to apply them before closing?"), _caption.c_str()), _("Apply"), _("Cancel"), _("Don't Apply")); switch (r) { case mforms::ResultOk: // Apply apply_changes(); res = !has_pending_changes(); break; case mforms::ResultCancel: res = false; break; case mforms::ResultOther: res = true; break; } } return res; } bool Recordset::close() { RETVAL_IF_FAIL_TO_RETAIN_RAW_PTR(Recordset, this, false) on_close(weak_ptr_from(this)); return true; } void Recordset::refresh() { if (has_pending_changes()) { task->send_msg(grt::ErrorMsg, ERRMSG_PENDING_CHANGES, _("Refresh Recordset")); return; } std::string data_search_string = _data_search_string; VarGridModel::refresh(); reset(); // reapply filter, if needed if (!data_search_string.empty()) set_data_search_string(data_search_string); if (rows_changed) rows_changed(); } void Recordset::rollback() { if (!reset(false)) task->send_msg(grt::ErrorMsg, _("Rollback failed"), _("Rollback recordset changes")); else refresh_ui(); } void Recordset::data_edited() { if (bec::GRTManager::get()->in_main_thread()) data_edited_signal(); else logError("data_edited called from thread\n"); } RowId Recordset::real_row_count() const { return _real_row_count; } void Recordset::recalc_row_count(sqlite::connection *data_swap_db) { // row count (visible rows only, some can be filtered out by applied column filters) { sqlite::query q(*data_swap_db, "select count(*) from `data_index`"); if (q.emit()) { std::shared_ptr<sqlite::result> rs = BoostHelper::convertPointer(q.get_result()); _row_count = rs->get_int(0); } else { _row_count = 0; } } // real row count (as if no column filters applied) { sqlite::query q(*data_swap_db, "select count(*) from `data`"); if (q.emit()) { std::shared_ptr<sqlite::result> rs = BoostHelper::convertPointer(q.get_result()); _real_row_count = rs->get_int(0); } else { _real_row_count = 0; } } } Recordset::Cell Recordset::cell(RowId row, ColumnId column) { if (_row_count == row) { RowId rowid = _next_new_rowid++; // rowid of the new record { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); sqlide::Sqlite_transaction_guarder transaction_guarder(data_swap_db.get()); // insert new empty data record { std::list<sqlite::variant_t> bind_vars; bind_vars.push_back((int)rowid); emit_partition_commands(data_swap_db.get(), data_swap_db_partition_count(), "insert into `data%s` (id) values (?)", bind_vars); } // insert new data index record { sqlite::command insert_data_index_record_statement(*data_swap_db, "insert into `data_index` (id) values (?)"); insert_data_index_record_statement % (int)rowid; insert_data_index_record_statement.emit(); } // log insert action { sqlite::command add_change_record_statement(*data_swap_db, _add_change_record_statement); add_change_record_statement % (int)rowid; add_change_record_statement % 1; static sqlite::null_type null_obj; add_change_record_statement % null_obj; add_change_record_statement.emit(); } transaction_guarder.commit(); } _data.resize(_data.size() + _column_count); ++_row_count; // init new row fields with null-values Cell new_cell = _data.begin() + (_data.size() - _column_count); for (ColumnId col = 0; _column_count > col; ++col, ++new_cell) *(new_cell) = sqlite::null_t(); _data[_data.size() - _column_count + _rowid_column] = (int)rowid; if (rows_changed) rows_changed(); } return VarGridModel::cell(row, column); } void Recordset::after_set_field(const NodeId &node, ColumnId column, const sqlite::variant_t &value) { VarGridModel::after_set_field(node, column, value); mark_dirty(node[0], column, value); data_edited(); tree_changed(); } void Recordset::mark_dirty(RowId row, ColumnId column, const sqlite::variant_t &new_value) { base::RecMutexLock data_mutex(_data_mutex); RowId rowid(row); NodeId node(row); if (get_field_(node, _rowid_column, (ssize_t &)rowid)) { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); sqlide::Sqlite_transaction_guarder transaction_guarder(data_swap_db.get()); // update record { size_t partition = data_swap_db_column_partition(column); std::string partition_suffix = data_swap_db_partition_suffix(partition); std::string sql = strfmt("update `data%s` set `_%u`=? where `id`=?", partition_suffix.c_str(), (unsigned int)column); sqlite::command update_data_record_statement(*data_swap_db, sql); sqlide::BindSqlCommandVar bind_sql_command_var(&update_data_record_statement); boost::apply_visitor(bind_sql_command_var, new_value); update_data_record_statement % (int)rowid; update_data_record_statement.emit(); } // log update action { sqlite::command add_data_change_record_statement(*data_swap_db, _add_change_record_statement); add_data_change_record_statement % (int)rowid; add_data_change_record_statement % 0; add_data_change_record_statement % (int)column; add_data_change_record_statement.emit(); } transaction_guarder.commit(); } } std::string Recordset::caption() { return base::strfmt("%s%s", _caption.c_str(), has_pending_changes() ? "*" : ""); } bool Recordset::delete_node(const bec::NodeId &node) { std::vector<bec::NodeId> nodes(1, node); return delete_nodes(nodes); } bool Recordset::delete_nodes(std::vector<bec::NodeId> &nodes) { { base::RecMutexLock data_mutex(_data_mutex); { std::sort(nodes.begin(), nodes.end()); std::vector<bec::NodeId>::iterator i = std::unique(nodes.begin(), nodes.end()); nodes.erase(i, nodes.end()); } RowId processed_node_count = 0; for (auto &node : nodes) { RowId row = node[0] - processed_node_count; if (!node.is_valid() || (row >= _row_count)) return false; } for (auto &node : nodes) { node[0] -= processed_node_count; RowId row = node[0]; ssize_t rowid; if (get_field_(node, _rowid_column, rowid)) { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); sqlide::Sqlite_transaction_guarder transaction_guarder(data_swap_db.get()); // save copy of the record being deleted for (size_t partition = 0, partition_count = data_swap_db_partition_count(); partition < partition_count; ++partition) { std::string partition_suffix = data_swap_db_partition_suffix(partition); sqlite::command save_deleted_data_record_statement( *data_swap_db, strfmt("insert into `deleted_rows%s` select * from `data%s` where id=?", partition_suffix.c_str(), partition_suffix.c_str())); save_deleted_data_record_statement % (int)rowid; save_deleted_data_record_statement.emit(); } // delete data record { std::list<sqlite::variant_t> bind_vars; bind_vars.push_back((int)rowid); emit_partition_commands(data_swap_db.get(), data_swap_db_partition_count(), "delete from `data%s` where id=?", bind_vars); } // delete data index record { sqlite::command delete_data_index_record_statement(*data_swap_db, "delete from `data_index` where id=?"); delete_data_index_record_statement % (int)rowid; delete_data_index_record_statement.emit(); } // log delete action { sqlite::command add_change_record_statement(*data_swap_db, _add_change_record_statement); add_change_record_statement % (int)rowid; add_change_record_statement % -1; static sqlite::null_type null_obj; add_change_record_statement % null_obj; add_change_record_statement.emit(); } transaction_guarder.commit(); --_row_count; --_data_frame_end; // delete record from cached data frame { Cell row_begin = _data.begin() + (row - _data_frame_begin) * _column_count; _data.erase(row_begin, row_begin + _column_count); } ++processed_node_count; } } nodes.clear(); } if (rows_changed) rows_changed(); data_edited(); return true; } bool Recordset::has_pending_changes() { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); if (data_swap_db) { sqlite::query check_pending_changes_statement(*data_swap_db, "select exists(select 1 from `changes`)"); std::shared_ptr<sqlite::result> rs = BoostHelper::convertPointer(check_pending_changes_statement.emit_result()); return (rs->get_int(0) == 1); } else { return false; } } void Recordset::pending_changes(int &upd_count, int &ins_count, int &del_count) const { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); std::string count_pending_changes_statement_sql = "select 1, (select count(*) from `data` where id>=?)\n" "union all\n" "select -1, (select count(*) from `deleted_rows` where id<?)\n" "union all\n" "select 0, (select count(1) from\n" "(select `record` from `changes` where `action`=0 and `record`<? group by `record`\n" "except\n" "select id from `deleted_rows`))"; sqlite::query count_pending_changes_statement(*data_swap_db, count_pending_changes_statement_sql); count_pending_changes_statement % (int)_min_new_rowid; count_pending_changes_statement % (int)_min_new_rowid; count_pending_changes_statement % (int)_min_new_rowid; std::shared_ptr<sqlite::result> rs = BoostHelper::convertPointer(count_pending_changes_statement.emit_result()); do { switch (rs->get_int(0)) { case 0: upd_count = rs->get_int(1); break; case 1: ins_count = rs->get_int(1); break; case -1: del_count = rs->get_int(1); break; } } while (rs->next_row()); } grt::StringRef Recordset::do_apply_changes(Ptr self_ptr, Recordset_data_storage::Ptr data_storage_ptr, bool skip_commit) { RETVAL_IF_FAIL_TO_RETAIN_WEAK_PTR(Recordset, self_ptr, self, grt::StringRef("")) RETVAL_IF_FAIL_TO_RETAIN_WEAK_PTR(Recordset_data_storage, data_storage_ptr, data_storage, grt::StringRef("")) try { data_storage->apply_changes(self_ptr, skip_commit); if (skip_commit) task->send_msg(grt::InfoMsg, _("Apply complete"), _("Applied but did not commit recordset changes")); else task->send_msg(grt::InfoMsg, _("Apply complete"), _("Applied and commited recordset changes")); reset(data_storage_ptr, false); } CATCH_AND_DISPATCH_EXCEPTION(false, "Apply recordset changes") return grt::StringRef(""); } /* * Actually applies recordset changes. Must run in the main thread for UI updates. */ void Recordset::apply_changes_(Recordset_data_storage::Ptr data_storage_ptr) { Recordset_data_storage::Ref storage = data_storage_ptr.lock(); try { storage->apply_changes(weak_ptr_from(this), false); reset(data_storage_ptr, false); // This message is nowhere shown and only the unit tests take notice. task->send_msg(grt::InfoMsg, _("Apply complete"), _("Applied and commited recordset changes")); on_apply_changes_finished(); } CATCH_AND_DISPATCH_EXCEPTION(false, "Apply recordset changes") } static int process_task_msg(int msgType, const std::string &message, const std::string &detail, int &error_count, std::string &messages_out) { if (msgType == grt::ErrorMsg) error_count++; if (!message.empty()) { if (!messages_out.empty()) messages_out.append("\n"); messages_out.append(message); } return 0; } bool Recordset::apply_changes_and_gather_messages(std::string &messages) { int error_count = 0; GrtThreadedTask::Msg_cb cb(task->msg_cb()); task->msg_cb(std::bind(process_task_msg, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::ref(error_count), std::ref(messages))); apply_changes(); task->msg_cb(cb); return error_count == 0; } void Recordset::rollback_and_gather_messages(std::string &messages) { int error_count = 0; GrtThreadedTask::Msg_cb cb(task->msg_cb()); task->msg_cb(std::bind(process_task_msg, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::ref(error_count), std::ref(messages))); rollback(); task->msg_cb(cb); } int Recordset::on_apply_changes_finished() { task->finish_cb(GrtThreadedTask::Finish_cb()); if (rows_changed) rows_changed(); data_edited(); return refresh_ui(); } void Recordset::apply_changes_() { apply_changes_(_data_storage); } bool Recordset::limit_rows() { return (_data_storage ? _data_storage->limit_rows() : false); } void Recordset::limit_rows(bool value) { if (has_pending_changes()) { task->send_msg(grt::ErrorMsg, ERRMSG_PENDING_CHANGES, _("Limit Rows")); return; } if (_data_storage) { if (_data_storage->limit_rows() != value) { _data_storage->limit_rows(value); refresh(); } } } void Recordset::toggle_limit_rows() { limit_rows(!limit_rows()); } void Recordset::scroll_rows_frame_forward() { if (_data_storage) { _data_storage->scroll_rows_frame_forward(); refresh(); } } void Recordset::scroll_rows_frame_backward() { if (_data_storage && (_data_storage->limit_rows_offset() != 0)) { _data_storage->scroll_rows_frame_backward(); refresh(); } } int Recordset::limit_rows_count() { return (_data_storage ? _data_storage->limit_rows_count() : 0); } void Recordset::limit_rows_count(int value) { if (_data_storage) _data_storage->limit_rows_count(value); } bool Recordset::limit_rows_applicable() { if (_data_storage && !_data_storage->limit_rows_applicable()) return false; bool limit_rows_ = limit_rows(); size_t limit_rows_count_ = limit_rows_count(); size_t row_count_ = real_row_count(); return (limit_rows_ && (row_count_ == limit_rows_count_)) || (!limit_rows_ && (row_count_ > limit_rows_count_)) || (0 < _data_storage->limit_rows_offset()); } Recordset_data_storage::Ref Recordset::data_storage_for_export(const std::string &format) { _data_storage_for_export.reset(); { std::vector<Recordset_storage_info> storage_types(Recordset_text_storage::storage_types()); for (std::vector<Recordset_storage_info>::const_iterator i = storage_types.begin(); i != storage_types.end(); ++i) { if (i->name == format) { Recordset_text_storage::Ref ds(Recordset_text_storage::create()); ds->data_format(format); _data_storage_for_export = ds; break; } } } if (_data_storage_for_export) return _data_storage_for_export; throw std::runtime_error(strfmt("Data storage format is not supported: %s", format.c_str())); } std::vector<Recordset_storage_info> Recordset::data_storages_for_export() { std::vector<Recordset_storage_info> storage_types; storage_types = Recordset_text_storage::storage_types(); return storage_types; } void Recordset::sort_by(ColumnId column, int direction, bool retaining) { if (_column_count == 0) return; if (!retaining) { _sort_columns.clear(); if (!(direction)) { std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); refresh_ui(); // refresh the sort indicators in column headers return; } } bool sort_column_exists = false; bool is_resort_needed = true; for (SortColumns::iterator sort_column = _sort_columns.begin(), end = _sort_columns.end(); sort_column != end; ++sort_column) { if (sort_column->first == column) { if ((direction)) { sort_column->second = direction; sort_column_exists = true; } else { if (_sort_columns.rbegin()->first == column) is_resort_needed = false; _sort_columns.erase(sort_column); } break; } } if (!sort_column_exists && (direction)) _sort_columns.push_back(std::make_pair(column, direction)); if (!is_resort_needed || _sort_columns.empty()) return; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } std::string Recordset::get_column_filter_expr(ColumnId column) const { Column_filter_expr_map::const_iterator i = _column_filter_expr_map.find(column); if (i != _column_filter_expr_map.end()) return i->second; return ""; } bool Recordset::has_column_filters() const { return !_column_filter_expr_map.empty(); } bool Recordset::has_column_filter(ColumnId column) const { Column_filter_expr_map::const_iterator i = _column_filter_expr_map.find(column); return (i != _column_filter_expr_map.end()); } void Recordset::reset_column_filters() { _column_filter_expr_map.clear(); std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } void Recordset::reset_column_filter(ColumnId column) { Column_filter_expr_map::iterator i = _column_filter_expr_map.find(column); if (i == _column_filter_expr_map.end()) return; _column_filter_expr_map.erase(i); std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } void Recordset::set_column_filter(ColumnId column, const std::string &filter_expr) { if (column >= get_column_count()) return; Column_filter_expr_map::const_iterator i = _column_filter_expr_map.find(column); if ((i != _column_filter_expr_map.end()) && (i->second == filter_expr)) return; _column_filter_expr_map[column] = filter_expr; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } size_t Recordset::column_filter_icon_id() const { IconManager *icon_man = IconManager::get_instance(); return icon_man->get_icon_id("tiny_search.png"); } const std::string &Recordset::data_search_string() const { return _data_search_string; } void Recordset::set_data_search_string(const std::string &value) { if (value == _data_search_string) return; _data_search_string = value; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } void Recordset::reset_data_search_string() { if (_data_search_string.empty()) return; _data_search_string.clear(); std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); rebuild_data_index(data_swap_db.get(), true, true); } void Recordset::rebuild_data_index(sqlite::connection *data_swap_db, bool do_cache_data_frame, bool do_refresh_ui) { { base::RecMutexLock data_mutex(_data_mutex); std::string where_clause; { sqlide::QuoteVar qv; { qv.escape_string = std::bind(sqlide::QuoteVar::escape_ansi_sql_string, std::placeholders::_1); qv.store_unknown_as_string = true; qv.allow_func_escaping = false; } sqlite::variant_t var_string_type = std::string(); sqlite::variant_t var_string; std::string sql_string; // column filters subclause std::string where_subclause1; { for (auto &column_filter_expr : _column_filter_expr_map) { var_string = column_filter_expr.second; sql_string = boost::apply_visitor(qv, var_string_type, var_string); where_subclause1 += strfmt("_%u like %s and ", (unsigned int)column_filter_expr.first, sql_string.c_str()); } if (!where_subclause1.empty()) { where_subclause1.resize(where_subclause1.size() - std::string(" and ").size()); where_subclause1.insert(0, "("); where_subclause1.append(")"); } } // data search subclause std::string where_subclause2; if (!_data_search_string.empty()) { var_string = "%" + _data_search_string + "%"; sql_string = boost::apply_visitor(qv, var_string_type, var_string); for (ColumnId column = 0, column_count = get_column_count(); column < column_count; ++column) { where_subclause2 += strfmt("_%u like %s or ", (unsigned int)column, sql_string.c_str()); } if (!where_subclause2.empty()) { where_subclause2.resize(where_subclause2.size() - std::string(" or ").size()); where_subclause2.insert(0, "("); where_subclause2.append(")"); } } if (!where_subclause1.empty() || !where_subclause2.empty()) { std::string subclauses_mediator = (!where_subclause1.empty() && !where_subclause2.empty()) ? " and " : ""; where_clause = strfmt("where %s%s%s", where_subclause1.c_str(), subclauses_mediator.c_str(), where_subclause2.c_str()); } } std::string orderby_clause; { for (auto &sort_column : _sort_columns) { std::string column_expr; switch (get_real_column_type(sort_column.first)) { case NumericType: case FloatType: case DatetimeType: column_expr = strfmt("cast(_%u as numeric)", (unsigned int)sort_column.first); break; case StringType: column_expr = strfmt("_%u COLLATE NOCASE", (unsigned int)sort_column.first); break; default: column_expr = strfmt("_%u", (unsigned int)sort_column.first); break; } const char *dir; switch (sort_column.second) { case 1: dir = "ASC"; break; case -1: dir = "DESC"; break; default: dir = ""; break; } orderby_clause += strfmt("%s %s, ", column_expr.c_str(), dir); } if (!orderby_clause.empty()) { orderby_clause.resize(orderby_clause.size() - std::string(", ").size()); orderby_clause.insert(0, "order by "); } } std::string tables_join = "`data`"; { for (size_t partition = 1, partition_count = data_swap_db_partition_count(); partition < partition_count; ++partition) { std::string partition_suffix = data_swap_db_partition_suffix(partition); tables_join += strfmt(" inner join `data%s` on (`data`.id=`data%s`.id)", partition_suffix.c_str(), partition_suffix.c_str()); } } { sqlide::Sqlite_transaction_guarder transaction_guarder(data_swap_db); std::string temp_table_name = "`data_index_" + grt::get_guid() + "`"; sqlite::execute(*data_swap_db, strfmt("create table if not exists %s (`id` integer)", temp_table_name.c_str()), true); sqlite::execute(*data_swap_db, strfmt("insert into %s select `data`.`id` from %s %s %s", temp_table_name.c_str(), tables_join.c_str(), where_clause.c_str(), orderby_clause.c_str()), true); sqlite::execute(*data_swap_db, "drop table if exists `data_index`", true); sqlite::execute(*data_swap_db, strfmt("alter table %s rename to `data_index`", temp_table_name.c_str()), true); transaction_guarder.commit(); } recalc_row_count(data_swap_db); if (do_cache_data_frame && _column_count > 0) cache_data_frame(0, true); } if (do_refresh_ui) refresh_ui(); } void Recordset::paste_rows_from_clipboard(ssize_t dest_row) { std::string text = mforms::Utilities::get_clipboard_text(); std::vector<std::string> rows; if (text.find("\r\n") != std::string::npos) rows = base::split(text, "\r\n"); else rows = base::split(text, "\n"); if (rows.empty()) return; if (rows.back().empty()) rows.pop_back(); if (dest_row < 0 || dest_row == (ssize_t)count() - 1) dest_row = count() - 1; else { if (rows.size() > 1) { if (mforms::Utilities::show_message_and_remember( "Paste Rows", "Cannot paste more than one row into an existing row, would you like to append them?", "Append", "Cancel", "", "Recordset.appendMultipleRowsOnPaste", "") != mforms::ResultOk) return; dest_row = count() - 1; } } int separator = ','; if (text.find('\t') != std::string::npos) separator = '\t'; for (std::vector<std::string>::const_iterator row = rows.begin(); row != rows.end(); ++row) { if (!row->empty()) { std::vector<std::string> parts = base::split_token_list(*row, separator); if (parts.size() != get_column_count()) { mforms::Utilities::show_error( "Cannot Paste Row", strfmt("Number of fields in pasted data doesn't match the columns in the table (%zi vs %zi).\n" "Data must be in the same format used by the Copy Row Content command.", parts.size(), get_column_count()), "OK"); if (rows_changed && row != rows.begin()) rows_changed(); return; } int i = 0; for (std::vector<std::string>::const_iterator p = parts.begin(); p != parts.end(); ++p, ++i) { std::string token = base::trim(*p); if (token == "NULL") set_field_null(dest_row, i); else { if (!token.empty() && token[0] == '\'' && token[token.size() - 1] == '\'') token = token.substr(1, token.size() - 2); set_field(dest_row, i, base::unescape_sql_string(token, '\'')); } } dest_row++; } } if (rows_changed) rows_changed(); } void Recordset::showPointInBrowser(const bec::NodeId &node, ColumnId column) { base::RecMutexLock data_mutex(_data_mutex); if (sqlide::is_var_blob(_real_column_types[column])) { std::string geometry; if (get_raw_field(node, column, geometry) && !geometry.empty()) { spatial::Importer importer; if (!importer.import_from_mysql(geometry)) { if (importer.getType() == spatial::ShapePoint) { std::deque<spatial::ShapeContainer> tmpShapes; importer.get_points(tmpShapes); if (tmpShapes.size() == 1 && tmpShapes[0].points.size() == 1) { std::string url = bec::GRTManager::get()->get_app_option_string("SqlEditor:geographicLocationURL"); if (url.empty()) { logError("Got empty url when trying to access geographicLocationURL\n"); mforms::Utilities::show_error( "Invalid Browser Location", "Point URL option have to be specified in the preferences to use this functionality.", "OK"); return; } url = base::replaceString(url, "%LAT%", base::to_string(tmpShapes[0].points[0].y)); url = base::replaceString(url, "%LON%", base::to_string(tmpShapes[0].points[0].x)); logDebug3("Opening url: %s\n", url.c_str()); mforms::Utilities::open_url(url); } else { logDebug3("Invalid column specified to showPointInBrowser.\n"); mforms::Utilities::show_error("Invalid Column", "A geometry type column is required to use this functionality.", "OK"); } } else { logError("Invalid column specified to showPointInBrowser, expected POINT got %s.\n", importer.getName().c_str()); mforms::Utilities::show_error("Invalid Column", "This functionality works only with Points", "OK"); } } else { logError("Unable to load geometry data\n"); mforms::Utilities::show_error("Invalid Column", "Unable to load geometry data", "OK"); } } } else { logDebug3("Invalid column specified to show point in browser\n"); mforms::Utilities::show_error("Invalid Column", "A geometry type column is required to use this functionality.", "OK"); } } mforms::ContextMenu *Recordset::get_context_menu() { if (!_context_menu) _context_menu = new mforms::ContextMenu(); return _context_menu; } void Recordset::update_selection_for_menu(const std::vector<int> &rows, int clicked_column) { // TODO: lift the restriction to a single column. // We need to support multiple cells (in multiple columns) on all platforms. _selected_rows = rows; _selected_column = clicked_column; if (_context_menu) { _context_menu->remove_all(); bool ro = is_readonly(); mforms::MenuItem *item = mforms::manage(new mforms::MenuItem(ro ? "Open Value in Viewer" : "Open Value in Editor")); item->set_name("Edit Cell"); // action in higher level item->setInternalName("edit_cell"); item->set_enabled((rows.size() == 1) && (clicked_column >= 0)); if (item->get_enabled()) { switch (get_real_column_type(clicked_column)) { case StringType: case BlobType: break; default: item->set_enabled(false); break; } } _context_menu->add_item(item); if (clicked_column >= 0 && isGeometry(clicked_column)) { item = _context_menu->add_item_with_title("Show point in browser", std::bind(&Recordset::activate_menu_item, this, "show_in_browser", rows, clicked_column), "Show point in browser", "show_in_browser"); } _context_menu->add_separator(); item = _context_menu->add_item_with_title( "Set Field to NULL", std::bind(&Recordset::activate_menu_item, this, "set_to_null", rows, clicked_column), "Set Field to NULL", "set_to_null"); // On Windows we can select individual cells, so it is perfectly ok to allow acting on multiple // cells. The other platforms only select entire rows in the multi-selection case. // So we to have disallow acting on them to avoid changing unrelated entries. #ifdef _MSC_VER item->set_enabled(clicked_column >= 0 && !ro); #else item->set_enabled(clicked_column >= 0 && rows.size() == 1 && !ro); #endif item = _context_menu->add_item_with_title( "Mark Field Value as a Function/Literal", std::bind(&Recordset::activate_menu_item, this, "set_to_function", rows, clicked_column), "Mark Fiels as Function or Literal", "set_to_function"); #ifdef _MSC_VER item->set_enabled(clicked_column >= 0 && !ro); #else item->set_enabled(clicked_column >= 0 && rows.size() == 1 && !ro); #endif item = _context_menu->add_item_with_title( "Delete Row(s)", std::bind(&Recordset::activate_menu_item, this, "delete_row", rows, clicked_column), "Delete Rows", "delete_row"); item->set_enabled(rows.size() > 0 && !ro); _context_menu->add_separator(); item = _context_menu->add_item_with_title( "Load Value From File...", std::bind(&Recordset::activate_menu_item, this, "load_from_file", rows, clicked_column), "Load Value From File", "load_from_file"); item->set_enabled(clicked_column >= 0 && rows.size() == 1 && !ro); item = _context_menu->add_item_with_title( "Save Value To File...", std::bind(&Recordset::activate_menu_item, this, "save_to_file", rows, clicked_column), "Save Value To File", "save_to_file"); item->set_enabled(clicked_column >= 0 && rows.size() == 1 && !ro); _context_menu->add_separator(); item = _context_menu->add_item_with_title( "Copy Row", std::bind(&Recordset::activate_menu_item, this, "copy_row", rows, clicked_column), "Copy Row", "copy_row"); item->set_enabled(rows.size() > 0); item = _context_menu->add_item_with_title( "Copy Row (with names)", std::bind(&Recordset::activate_menu_item, this, "copy_row_with_names", rows, clicked_column), "Copy Row With Names", "copy_row_with_names"); item = _context_menu->add_item_with_title( "Copy Row (unquoted)", std::bind(&Recordset::activate_menu_item, this, "copy_row_unquoted", rows, clicked_column), "Copy Row Unquoted", "copy_row_unquoted"); item->set_enabled(rows.size() > 0); item = _context_menu->add_item_with_title( "Copy Row (with names, unquoted)", std::bind(&Recordset::activate_menu_item, this, "copy_row_unquoted_with_names", rows, clicked_column), "Copy Row With Names and Unquoted)", "copy_row_unquoted_with_names"); item = _context_menu->add_item_with_title( "Copy Row (with names, tab separated)", std::bind(&Recordset::activate_menu_item, this, "copy_row_with_names_tabsep", rows, clicked_column), "Copy Row with Names Tab Separated", "copy_row_with_names_tabsep"); item->set_enabled(rows.size() > 0); item = _context_menu->add_item_with_title( "Copy Row (tab separated)", std::bind(&Recordset::activate_menu_item, this, "copy_row_tabsep", rows, clicked_column), "Copy Row Tab Separated", "copy_row_tabsep"); item->set_enabled(rows.size() > 0); item = _context_menu->add_item_with_title( "Copy Field", std::bind(&Recordset::activate_menu_item, this, "copy_field", rows, clicked_column), "Copy Field", "copy_field"); item->set_enabled(clicked_column >= 0 && rows.size() == 1); item = _context_menu->add_item_with_title( "Copy Field (unquoted)", std::bind(&Recordset::activate_menu_item, this, "copy_field_unquoted", rows, clicked_column), "Copy Field (unquoted)", "copy_field_unquoted"); item->set_enabled(clicked_column >= 0 && rows.size() == 1); item = _context_menu->add_item_with_title( "Paste Row", std::bind(&Recordset::activate_menu_item, this, "paste_row", rows, clicked_column), "Paste Row", "paste_row"); item->set_enabled(rows.size() <= 1 && !mforms::Utilities::get_clipboard_text().empty() && !ro); if (update_selection_for_menu_extra) update_selection_for_menu_extra(_context_menu, rows, clicked_column); } } void Recordset::activate_menu_item(const std::string &action, const std::vector<int> &rows, int clicked_column) { bool need_ui_refresh = false; // TODO: the tests here for rows count and clicked_column are all unnecessary. This has already be done. if (action == "edit_cell") { if (rows.size() == 1 && clicked_column >= 0) { open_field_data_editor(rows[0], clicked_column, ""); } } else if (action == "set_to_null") { for (size_t i = 0; i < rows.size(); ++i) { bec::NodeId node; node.append(rows[i]); set_field_null(node, clicked_column); } } else if (action == "set_to_function") { for (size_t i = 0; i < rows.size(); ++i) { bec::NodeId node; Cell cell; node.append(rows[i]); std::string function; if (!get_cell(cell, node, clicked_column, false)) function = ""; else function = boost::apply_visitor(_var_to_str, *cell); if (!g_str_has_prefix(function.c_str(), "\\func")) set_field(node, clicked_column, std::string("\\func ") + function); } } else if (action == "delete_row") { if (rows.size() > 0) { std::vector<int> sorted_rows(rows); std::sort(sorted_rows.begin(), sorted_rows.end()); std::vector<bec::NodeId> nodes; for (std::vector<int>::reverse_iterator iter = sorted_rows.rbegin(); iter != sorted_rows.rend(); ++iter) nodes.push_back(bec::NodeId(*iter)); delete_nodes(nodes); need_ui_refresh = true; } } else if (action == "save_to_file") { if (rows.size() == 1 && clicked_column >= 0) { bec::NodeId node; node.append(rows[0]); save_to_file(node, clicked_column); } } else if (action == "load_from_file") { if (rows.size() == 1 && clicked_column >= 0) { bec::NodeId node; node.append(rows[0]); load_from_file(node, clicked_column); } } else if (action == "copy_row") { if (rows.size() > 0) { copy_rows_to_clipboard(rows, ", "); } } else if (action == "copy_row_with_names") { copy_rows_to_clipboard(rows, ", ", true, true); } else if (action == "copy_row_unquoted") { if (rows.size() > 0) { copy_rows_to_clipboard(rows, ", ", false); } } else if (action == "copy_row_unquoted_with_names") { copy_rows_to_clipboard(rows, ", ", false, true); } else if (action == "copy_row_with_names_tabsep") { copy_rows_to_clipboard(rows, "\t", false, true); } else if (action == "copy_row_tabsep") { if (rows.size() > 0) { copy_rows_to_clipboard(rows, "\t", false); } } else if (action == "copy_field") { if (rows.size() == 1 && clicked_column >= 0) { copy_field_to_clipboard(rows[0], clicked_column); } } else if (action == "copy_field_unquoted") { if (rows.size() == 1 && clicked_column >= 0) { copy_field_to_clipboard(rows[0], clicked_column, false); } } else if (action == "paste_row") { paste_rows_from_clipboard(rows.empty() ? -1 : rows[0]); need_ui_refresh = true; } else if (action == "show_in_browser") { if (rows.size() == 1 && clicked_column >= 0) { bec::NodeId node; node.append(rows[0]); showPointInBrowser(node, clicked_column); } } if (need_ui_refresh) refresh_ui(); } void Recordset::copy_rows_to_clipboard(const std::vector<int> &indeces, std::string sep, bool quoted, bool with_header) { ColumnId editable_col_count = get_column_count(); if (!editable_col_count) return; sqlide::QuoteVar qv; { qv.escape_string = std::bind(base::escape_sql_string, std::placeholders::_1, false); qv.store_unknown_as_string = true; qv.allow_func_escaping = true; } Cell cell; std::string text; if (with_header) { text = "# "; for (ColumnId col = 0; editable_col_count > col; ++col) { if (col > 0) text.append(sep); text.append(get_column_caption(col)); } text.append("\n"); } for (auto row : indeces) { std::string line; for (ColumnId col = 0; editable_col_count > col; ++col) { bec::NodeId node(row); if (!get_cell(cell, node, col, false)) continue; if (col > 0) line += sep; if (quoted) line += boost::apply_visitor(qv, _column_types[col], *cell); else line += boost::apply_visitor(_var_to_str, *cell); } if (!line.empty()) text += line + "\n"; } mforms::Utilities::set_clipboard_text(text); } void Recordset::copy_field_to_clipboard(int row, ColumnId column, bool quoted) { sqlide::QuoteVar qv; { qv.escape_string = std::bind(sqlide::QuoteVar::escape_ansi_sql_string, std::placeholders::_1); qv.store_unknown_as_string = true; qv.allow_func_escaping = true; } std::string text; bec::NodeId node(row); Cell cell; if (get_cell(cell, node, column, false)) { if (quoted) text = boost::apply_visitor(qv, _column_types[column], *cell); else text = boost::apply_visitor(_var_to_str, *cell); } mforms::Utilities::set_clipboard_text(text); } std::string Recordset::status_text() { std::string limit_text; if (limit_rows_applicable() && limit_rows()) limit_text = ", row LIMIT active"; else limit_text = ""; std::string skipped_row_count_text; if (_data_storage && _data_storage->limit_rows()) { int limit_rows_offset = _data_storage->limit_rows_offset(); if (limit_rows_offset > 0) skipped_row_count_text = strfmt(" after %i skipped", limit_rows_offset); } std::stringstream out; out << "Fetched " << real_row_count() << " records" << skipped_row_count_text << limit_text; std::string status_text = out.str(); { int upd_count = 0, ins_count = 0, del_count = 0; pending_changes(upd_count, ins_count, del_count); if (upd_count > 0) status_text += strfmt(", updated %i", upd_count); if (ins_count > 0) status_text += strfmt(", inserted %i", ins_count); if (del_count > 0) status_text += strfmt(", deleted %i", del_count); } status_text.append("."); if (!status_text_trailer.empty()) status_text.append(" ").append(status_text_trailer); return status_text; } static mforms::ToolBarItem *add_toolbar_action_item(mforms::ToolBar *toolbar, bec::IconManager *im, const std::string &accessibilityName, const std::string &item_icon, const std::string &item_name, const std::string &item_tooltip) { mforms::ToolBarItem *item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem)); item->set_name(accessibilityName); item->setInternalName(item_name); item->set_icon(im->get_icon_path(item_icon)); item->set_tooltip(item_tooltip); toolbar->add_item(item); return item; } static mforms::ToolBarItem *add_toolbar_action_item(mforms::ToolBar *toolbar, bec::IconManager *im, const std::string &accessibilityName, const std::string &item_name, const std::string &item_tooltip) { return add_toolbar_action_item(toolbar, im, accessibilityName, item_name + ".png", item_name, item_tooltip); } static void add_toolbar_label_item(mforms::ToolBar *toolbar, const std::string &label, const std::string &name) { mforms::ToolBarItem *item = mforms::manage(new mforms::ToolBarItem(mforms::LabelItem)); item->set_text(label); item->set_name(name); toolbar->add_item(item); } void Recordset::search_activated(mforms::ToolBarItem *item) { std::string text; if ((text = item->get_text()).empty()) reset_data_search_string(); else set_data_search_string(text); } void Recordset::rebuild_toolbar() { if (_toolbar) { _toolbar->remove_all(); // hack so that this label only appears for resultset grids and not inserts editor if (!_inserts_editor) { mforms::ToolBarItem *item = mforms::manage(new mforms::ToolBarItem(mforms::TitleItem)); ; item->set_text("Result Grid"); _toolbar->add_item(item); _toolbar->add_separator_item(); } mforms::ToolBarItem *item; bec::IconManager *im = bec::IconManager::get_instance(); item = add_toolbar_action_item(_toolbar, im, "Reset Record Sort", "record_sort_reset.png", "record_sort_reset", "Resets all sorted columns"); if (!_data_storage || _data_storage->reloadable()) { item = add_toolbar_action_item(_toolbar, im, "Refresh", "record_refresh.png", "record_refresh", "Refresh data re-executing the original query"); item->signal_activated()->connect(std::bind(&Recordset::refresh, this)); } add_toolbar_label_item(_toolbar, "Filter Rows:", "Filter Rows"); item = mforms::manage(new mforms::ToolBarItem(mforms::SearchFieldItem)); item->set_name("Search Field"); item->signal_activated()->connect(std::bind(&Recordset::search_activated, this, std::placeholders::_1)); _toolbar->add_item(item); if (!is_readonly() || _inserts_editor) { _toolbar->add_separator_item(); add_toolbar_label_item(_toolbar, "Edit:", "Edit"); add_toolbar_action_item(_toolbar, im, "Edit Row", "record_edit", "Edit current row"); // connect in frontend add_toolbar_action_item(_toolbar, im, "Insert Row", "record_add", "Insert new row"); // connect in frontend add_toolbar_action_item(_toolbar, im, "Delete Selected", "record_del", "Delete selected rows"); // connect in frontend } _toolbar->add_separator_item(); if (!is_readonly() || _inserts_editor) add_toolbar_label_item(_toolbar, "Export/Import:", "Export or Import"); else add_toolbar_label_item(_toolbar, "Export:", "Export or Import"); add_toolbar_action_item(_toolbar, im, "Export Record", "record_export", "Export recordset to an external file"); if (!is_readonly() || _inserts_editor) add_toolbar_action_item(_toolbar, im, "Record Import", "record_import", "Import records from an external file"); #ifndef __APPLE__ _toolbar->add_separator_item(); add_toolbar_label_item(_toolbar, "Wrap Cell Content:", "Wrap Cell Content"); add_toolbar_action_item(_toolbar, im, "Wrap Vertical", "record_wrap_vertical", "Toggle wrapping of cell contents"); // connect in frontend #endif if (limit_rows_applicable()) { _toolbar->add_separator_item(); add_toolbar_label_item(_toolbar, "Fetch rows:", "Fetch Rows"); item = add_toolbar_action_item(_toolbar, im, "Fetch Previous", "record_fetch_prev.png", "Fetch previous frame of records from the data source"); item->signal_activated()->connect(std::bind(&Recordset::scroll_rows_frame_backward, this)); item = add_toolbar_action_item(_toolbar, im, "Fetch Next", "record_fetch_next.png", "scroll_rows_frame_forward", "Fetch next frame of records from the data source"); item->signal_activated()->connect(std::bind(&Recordset::scroll_rows_frame_forward, this)); } if (_inserts_editor /* && !is_readonly()*/) { _toolbar->add_separator_item(); add_toolbar_label_item(_toolbar, "Apply changes:", "Apply Changes"); item = add_toolbar_action_item(_toolbar, im, "Save Record", "record_save", "Apply changes to data"); item->signal_activated()->connect(std::bind(&Recordset::apply_changes, this)); item = add_toolbar_action_item(_toolbar, im, "Discard Record", "record_discard", "Discard changes to data"); item->signal_activated()->connect(std::bind(&Recordset::rollback, this)); } } } mforms::ToolBar *Recordset::get_toolbar() { if (!_toolbar) { _toolbar = mforms::manage(new mforms::ToolBar(mforms::SecondaryToolBar)); rebuild_toolbar(); } return _toolbar; } void Recordset::apply_changes() { if (flush_ui_changes_cb) flush_ui_changes_cb(); apply_changes_cb(); // If the SQL IDE redirects apply_changes_cb() we won't get a call to the task finish callback. // This causes some other notifications not to be called (especially changed rows). // Currently this callback is always redirected, so we can assume rows_changed() is not called multiple times. if (rows_changed) rows_changed(); } ActionList &Recordset::action_list() { return _action_list; } void Recordset::register_default_actions() { _action_list.register_action("record_sort_reset", std::bind(&Recordset::sort_by, this, 0, 0, false)); _action_list.register_action("scroll_rows_frame_forward", std::bind(&Recordset::scroll_rows_frame_forward, this)); _action_list.register_action("scroll_rows_frame_backward", std::bind(&Recordset::scroll_rows_frame_backward, this)); _action_list.register_action("record_fetch_all", std::bind(&Recordset::toggle_limit_rows, this)); _action_list.register_action("record_refresh", std::bind(&Recordset::refresh, this)); } class DataEditorSelector : public boost::static_visitor<BinaryDataEditor *> { public: DataEditorSelector(bool read_only) : _read_only(read_only) { } DataEditorSelector(bool read_only, const std::string &encoding, const std::string &type) : _encoding(encoding), _type(type), _read_only(read_only) { } const std::string &encoding() const { return _encoding; } void encoding(const std::string &value) { _encoding = value; } private: std::string _encoding; std::string _type; bool _read_only; public: result_type operator()(const sqlite::null_t &v) { return new BinaryDataEditor(NULL, 0, _encoding, _type, _read_only); } result_type operator()(const std::string &v) { return new BinaryDataEditor(v.c_str(), v.length(), _encoding, _type, _read_only); } result_type operator()(const sqlite::blob_ref_t &v) { return new BinaryDataEditor(((!v || v->empty()) ? NULL : (const char *)&(*v)[0]), v->size(), _encoding, _type, _read_only); } template <typename V> result_type operator()(const V &v) { return NULL; } }; class DataEditorSelector2 : public boost::static_visitor<BinaryDataEditor *> { public: DataEditorSelector2(bool read_only, const std::string &type) : _type(type), _read_only(read_only) { } private: std::string _type; bool _read_only; public: template <typename V> result_type operator()(const std::string &t, const V &v) { return DataEditorSelector(_read_only, "UTF-8", _type)(v); } template <typename V> result_type operator()(const sqlite::blob_ref_t &t, const V &v) { return DataEditorSelector(_read_only, "LATIN1", _type)(v); } template <typename T, typename V> result_type operator()(const T &r, const V &v) { // return NULL; // For unknown types treat them for now as string values. Since we have a binary editor pane there that should work // all the time well enough. return DataEditorSelector(_read_only, "UTF-8", _type)(v); } }; void Recordset::open_field_data_editor(RowId row, ColumnId column, const std::string &logical_type) { base::RecMutexLock data_mutex(_data_mutex); try { sqlite::variant_t blob_value; sqlite::variant_t *value; if (sqlide::is_var_blob(_real_column_types[column])) { if (!_data_storage) return; RowId rowid; NodeId node(row); if (!get_field_(node, _rowid_column, (ssize_t &)rowid)) return; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); _data_storage->fetch_blob_value(this, data_swap_db.get(), rowid, column, blob_value); value = &blob_value; } else { Cell cell; bec::NodeId node(row); if (!get_cell(cell, node, column, false)) return; value = &(*cell); } DataEditorSelector2 data_editor_selector2(is_readonly(), logical_type); BinaryDataEditor *data_editor = boost::apply_visitor(data_editor_selector2, _real_column_types[column], *value); if (!data_editor) return; data_editor->set_title(base::strfmt("Edit Data for %s (%s)", _column_names[column].c_str(), logical_type.c_str())); data_editor->set_release_on_close(true); data_editor->signal_saved.connect(std::bind(&Recordset::set_field_value, this, row, column, data_editor)); data_editor->show(true); } CATCH_AND_DISPATCH_EXCEPTION(false, "Open field editor") } class DataValueConv : public boost::static_visitor<sqlite::variant_t> { public: DataValueConv() : _data(NULL), _length(0) { } DataValueConv(const char *data, size_t length) { set_data(data, length); } void set_data(const char *data, size_t length) { _data = data; _length = length; } private: const char *_data; size_t _length; public: result_type operator()(const sqlite::blob_ref_t &t) { sqlite::blob_ref_t val = sqlite::blob_ref_t(new sqlite::blob_t()); val->resize(_length); memcpy(&(*val)[0], _data, _length); return val; } result_type operator()(const std::string &t) { return std::string(_data, _length); } template <typename T> result_type operator()(const T &t) { return sqlite::unknown_t(); } }; void Recordset::set_field_value(RowId row, ColumnId column, BinaryDataEditor *data_editor) { if (!data_editor) return; set_field_raw_data(row, column, data_editor->data(), data_editor->length(), data_editor->isJson()); } void Recordset::set_field_raw_data(RowId row, ColumnId column, const char *data, size_t data_length, bool isJson /*= false*/) { DataValueConv data_value_conv(data, data_length); sqlite::variant_t valueString = std::string(""); sqlite::variant_t value = boost::apply_visitor(data_value_conv, isJson ? valueString : _real_column_types[column]); if (sqlide::is_var_unknown(value)) throw std::logic_error("Can't save value of this data type."); bec::NodeId node(row); set_field(node, column, value); } void Recordset::load_from_file(const bec::NodeId &node, ColumnId column, const std::string &file) { char *data; gsize length; GError *error = 0; if (!g_file_get_contents(file.c_str(), &data, &length, &error)) { mforms::Utilities::show_error("Cannot Load Field Value", error ? error->message : "Error loading file data", "OK"); return; } try { set_field_raw_data(node[0], column, data, length); } catch (const std::exception &exc) { mforms::Utilities::show_error("Cannot Load Field Value", exc.what(), "OK", "", ""); } } void Recordset::load_from_file(const bec::NodeId &node, ColumnId column) { mforms::FileChooser chooser(mforms::OpenFile); chooser.set_title("Load Field Value"); if (chooser.run_modal()) load_from_file(node, column, chooser.get_path()); } class BlobCopier : public boost::static_visitor<void> { public: BlobCopier() { } std::ostringstream os; public: result_type operator()(const sqlite::blob_ref_t &v) { std::copy(v->begin(), v->end(), std::ostreambuf_iterator<char>(os)); } result_type operator()(const std::string &v) { os << v; } template <typename T> result_type operator()(const T &) { } }; bool Recordset::get_raw_field(const bec::NodeId &node, ColumnId column, std::string &data_ret) { base::RecMutexLock data_mutex(_data_mutex); sqlite::variant_t blob_value; sqlite::variant_t *value; if (sqlide::is_var_blob(_real_column_types[column])) { if (!_data_storage) return false; ssize_t rowid; if (!get_field_(node, _rowid_column, rowid)) return false; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); _data_storage->fetch_blob_value(this, data_swap_db.get(), rowid, column, blob_value); value = &blob_value; } else { Cell cell; if (!get_cell(cell, node, column, false)) return false; value = &(*cell); } BlobCopier copier; boost::apply_visitor(copier, *value); data_ret = copier.os.str(); return true; } class DataValueDump : public boost::static_visitor<void> { public: DataValueDump(const char *filename) : os(filename, std::ios_base::out | std::ios_base::binary) { } std::ofstream os; public: result_type operator()(const sqlite::blob_ref_t &v) { std::copy(v->begin(), v->end(), std::ostreambuf_iterator<char>(os)); } result_type operator()(const std::string &v) { os << v; } template <typename T> result_type operator()(const T &) { } }; void Recordset::save_to_file(const bec::NodeId &node, ColumnId column, const std::string &file) { base::RecMutexLock data_mutex(_data_mutex); sqlite::variant_t blob_value; sqlite::variant_t *value; if (sqlide::is_var_blob(_real_column_types[column])) { if (!_data_storage) return; ssize_t rowid; if (!get_field_(node, _rowid_column, rowid)) return; std::shared_ptr<sqlite::connection> data_swap_db = this->data_swap_db(); _data_storage->fetch_blob_value(this, data_swap_db.get(), rowid, column, blob_value); value = &blob_value; } else { Cell cell; if (!get_cell(cell, node, column, false)) return; value = &(*cell); } DataValueDump data_value_dump(file.c_str()); if (data_value_dump.os) { boost::apply_visitor(data_value_dump, *value); } } void Recordset::save_to_file(const bec::NodeId &node, ColumnId column) { mforms::FileChooser chooser(mforms::SaveFile); chooser.set_title("Save Field Value"); chooser.set_extensions("Text files (*.txt)|*.txt|All Files (*.*)|*.*", "txt"); if (chooser.run_modal()) { try { save_to_file(node, column, chooser.get_path()); } CATCH_AND_DISPATCH_EXCEPTION(false, "Save field to file") } }