pgLab/pglab/QueryTab.cpp
eelke 15bee33076 Made step to remove ASyncWindow in favour of usage of Future and FutureWatcher.
This should allow concurrency in the plugins to be independent from their container.

Contains also some work on the system for registering plugins.
2018-12-30 15:46:15 +01:00

701 lines
20 KiB
C++

#include "QueryTab.h"
#include "ui_QueryTab.h"
#include "SqlSyntaxHighlighter.h"
#include <QStandardPaths>
#include <QPushButton>
#include <QAction>
#include <QFileDialog>
#include <QStatusBar>
#include <QMessageBox>
#include <QTabWidget>
#include <QTextCodec>
#include <QTextDocumentFragment>
#include <QTextStream>
#include <QClipboard>
#include "ExplainTreeModelItem.h"
#include "json/json.h"
#include "OpenDatabase.h"
#include "catalog/PgDatabaseCatalog.h"
#include "QueryParamListController.h"
#include "util.h"
#include "GlobalIoService.h"
#include "UserConfiguration.h"
#include "IPluginContentWidgetContext.h"
#include "plugin_support/PluginRegister.h"
QueryTab::QueryTab(IPluginContentWidgetContext *context_, QWidget *parent)
: PluginContentWidget(context_, parent)
, ui(new Ui::QueryTab)
, m_dbConnection(*getGlobalAsioIoService())
{
ui->setupUi(this);
m_config = context()->getDatabase()->config();
m_catalog = context()->getDatabase()->catalog();
connect(&m_dbConnection, &ASyncDBConnection::onStateChanged, this, &QueryTab::connectionStateChanged);
connect(&m_dbConnection, &ASyncDBConnection::onNotice, this, &QueryTab::receiveNotice);
ui->queryEdit->setFont(UserConfiguration::instance()->codeFont());
highlighter = new SqlSyntaxHighlighter(ui->queryEdit->document());
auto open_database = context()->getDatabase();
if (open_database) {
auto cat = open_database->catalog();
highlighter->setTypes(*cat->types());
}
connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTab::queryTextChanged);
m_queryParamListController = new QueryParamListController(ui->paramTableView, open_database, this);
connect(ui->addButton, &QPushButton::clicked, m_queryParamListController,
&QueryParamListController::on_addParam);
connect(ui->removeButton, &QPushButton::clicked, m_queryParamListController,
&QueryParamListController::on_removeParam);
context()->QueueTask([this]() { startConnect(); });
}
QueryTab::~QueryTab()
{
// m_dbConnection.blockSignals(true);
// m_dbConnection.closeConnection();
delete ui;
}
bool QueryTab::canClose()
{
bool can_close;
if (m_queryTextChanged) {
can_close = continueWithoutSavingWarning();
}
else {
can_close = true;
}
return can_close;
}
void QueryTab::newdoc()
{
ui->queryEdit->clear();
setFileName(tr("new"));
m_queryTextChanged = false;
m_new = true;
}
bool QueryTab::load(const QString &filename)
{
bool result = false;
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
QByteArray ba = file.readAll();
const char *ptr = ba.constData();
QTextCodec *codec = QTextCodec::codecForUtfText(ba, QTextCodec::codecForName("utf-8"));
QTextCodec::ConverterState state;
QString text = codec->toUnicode(ptr, ba.size(), &state);
if (state.invalidChars > 0) {
file.reset();
QTextStream stream(&file);
text = stream.readAll();
}
ui->queryEdit->setPlainText(text);
m_queryTextChanged = false;
setFileName(filename);
m_new = false;
result = true;
}
return result;
}
bool QueryTab::save()
{
bool result;
if (m_fileName.isEmpty() || m_new) {
result = saveAs();
}
else {
result = saveSqlTo(m_fileName);
}
return result;
}
bool QueryTab::saveAs()
{
bool result = false;
QString filename = promptUserForSaveSqlFilename();
if (!filename.isEmpty()) {
result = saveSqlTo(filename);
if (result) {
setFileName(filename);
m_new = false;
}
}
return result;
}
void QueryTab::saveCopyAs()
{
QString filename = promptUserForSaveSqlFilename();
if (!filename.isEmpty()) {
saveSqlTo(filename);
}
}
void QueryTab::execute()
{
if (m_dbConnection.state() == ASyncDBConnection::State::Connected) {
addLog("Query clicked");
clearResult();
ui->messagesEdit->clear();
std::string cmd = getCommandUtf8();
m_stopwatch.start();
auto cb = [this](Expected<std::shared_ptr<Pgsql::Result>> res, qint64 elapsedms)
{
context()->QueueTask([this, res, elapsedms]() { query_ready(res, elapsedms); });
};
if (m_queryParamListController->empty())
m_dbConnection.send(cmd, cb);
else
m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
}
}
void QueryTab::explain(bool analyze)
{
ui->explainTreeView->setModel(nullptr);
explainModel.reset();
ui->messagesEdit->clear();
addLog("Explain clicked");
std::string analyze_str;
if (analyze) {
analyze_str = "ANALYZE, BUFFERS, ";
}
m_stopwatch.start();
std::string cmd = "EXPLAIN (" + analyze_str + "VERBOSE, FORMAT JSON) " + getCommandUtf8();
auto cb = [this](Expected<std::shared_ptr<Pgsql::Result>> exp_res, qint64 )
{
if (exp_res.valid()) {
// Process explain data seperately
auto res = exp_res.get();
if (res) {
std::thread([this,res]()
{
std::shared_ptr<ExplainRoot> explain;
if (res->cols() == 1 && res->rows() == 1) {
std::string s = res->val(0, 0);
Json::Value root; // will contains the root value after parsing.
Json::Reader reader;
bool parsingSuccessful = reader.parse(s, root);
if (parsingSuccessful) {
explain = ExplainRoot::createFromJson(root);
}
}
context()->QueueTask([this, explain]() { explain_ready(explain); });
}).detach();
}
}
};
if (m_queryParamListController->empty())
m_dbConnection.send(cmd, cb);
else
m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
}
void QueryTab::cancel()
{
m_dbConnection.cancel();
}
void QueryTab::setFileName(const QString &filename)
{
m_fileName = filename;
QFileInfo fileInfo(filename);
QString fn(fileInfo.fileName());
context()->setCaption(this, fn, m_fileName);
}
bool QueryTab::continueWithoutSavingWarning()
{
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Warning);
msgBox.setText(QString("Save changes in document \"%1\" before closing?").arg(m_fileName));
msgBox.setInformativeText("The changes will be lost when you choose Discard.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
int ret = msgBox.exec();
if (ret == QMessageBox::Save) {
if (!save()) {
// save failed or was a saveAs and was cancelled, don't close!
ret = QMessageBox::Cancel;
}
}
return ret != QMessageBox::Cancel;
}
bool QueryTab::saveSqlTo(const QString &filename)
{
bool result = false;
QFileInfo fileinfo(filename);
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream.setCodec("utf-8");
QString text = ui->queryEdit->toPlainText();
stream << text;
stream.flush();
if (stream.status() == QTextStream::Ok) {
m_queryTextChanged = false;
result = true;
}
}
return result;
}
QString QueryTab::promptUserForSaveSqlFilename()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)"));
}
void QueryTab::queryTextChanged()
{
m_queryTextChanged = true;
}
void QueryTab::connectionStateChanged(ASyncDBConnection::State state)
{
QString iconname;
switch (state) {
case ASyncDBConnection::State::NotConnected:
case ASyncDBConnection::State::Connecting:
iconname = "document_red.png";
break;
case ASyncDBConnection::State::Connected:
iconname = "document_green.png";
break;
case ASyncDBConnection::State::QuerySend:
case ASyncDBConnection::State::CancelSend:
iconname = "document_yellow.png";
break;
case ASyncDBConnection::State::Terminating:
break;
}
context()->setIcon(this, iconname);
}
void QueryTab::addLog(QString s)
{
QTextCursor text_cursor = QTextCursor(ui->edtLog->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertText(s + "\r\n");
}
void QueryTab::receiveNotice(Pgsql::ErrorDetails notice)
{
ui->messagesEdit->append(QString::fromStdString(notice.errorMessage));
ui->messagesEdit->append(QString::fromStdString(notice.severity));
ui->messagesEdit->append(QString("At position: %1").arg(notice.statementPosition));
ui->messagesEdit->append(QString::fromStdString("State: " + notice.state));
ui->messagesEdit->append(QString::fromStdString("Primary: " + notice.messagePrimary));
ui->messagesEdit->append(QString::fromStdString("Detail: " + notice.messageDetail));
ui->messagesEdit->append(QString::fromStdString("Hint: " + notice.messageHint));
ui->messagesEdit->append(QString::fromStdString("Context: " + notice.context));
// std::string state; ///< PG_DIAG_SQLSTATE Error code as listed in https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
// std::string severity;
// std::string messagePrimary;
// std::string messageDetail;
// std::string messageHint;
// int statementPosition; ///< First character is one, measured in characters not bytes!
// std::string context;
// int internalPosition;
// std::string internalQuery;
// std::string schemaName;
// std::string tableName;
// std::string columnName;
// std::string datatypeName;
// std::string constraintName;
// std::string sourceFile;
// std::string sourceLine;
// std::string sourceFunction;
// QTextCursor cursor = ui->messagesEdit->textCursor();
// cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
// QTextTable *table = cursor.insertTable(4, 2);
// if (table) {
// table->cellAt(1, 0).firstCursorPosition().insertText("State");
// table->cellAt(1, 1).firstCursorPosition().insertText(QString::fromStdString(notice.state));
// table->cellAt(2, 0).firstCursorPosition().insertText("Primary");
// table->cellAt(2, 1).firstCursorPosition().insertText(QString::fromStdString(notice.messagePrimary));
// table->cellAt(3, 0).firstCursorPosition().insertText("Detail");
// table->cellAt(3, 1).firstCursorPosition().insertText(QString::fromStdString(notice.messageDetail));
// }
// syntax error at or near "limit
// statementPosition
}
void QueryTab::startConnect()
{
m_dbConnection.setupConnection(m_config);
}
void QueryTab::explain_ready(ExplainRoot::SPtr explain)
{
m_stopwatch.stop();
if (explain) {
addLog("Explain ready");
QString times_str;
if (explain->totalRuntime > 0.f)
times_str = QString("Total time: %1").arg(
msfloatToHumanReadableString(explain->totalRuntime));
else
times_str = QString("Execution time: %1, Planning time: %2").arg(
msfloatToHumanReadableString(explain->executionTime)
, msfloatToHumanReadableString(explain->planningTime));
ui->lblTimes->setText(times_str);
explainModel.reset(new QueryExplainModel(nullptr, explain));
ui->explainTreeView->setModel(explainModel.get());
ui->explainTreeView->expandAll();
ui->explainTreeView->setColumnWidth(0, 200);
ui->explainTreeView->setColumnWidth(1, 80);
ui->explainTreeView->setColumnWidth(2, 80);
ui->explainTreeView->setColumnWidth(3, 80);
ui->explainTreeView->setColumnWidth(4, 80);
ui->explainTreeView->setColumnWidth(5, 80);
ui->explainTreeView->setColumnWidth(6, 600);
ui->tabWidget->setCurrentWidget(ui->explainTab);
context()->showStatusMessage(tr("Explain ready."));
}
else {
addLog("Explain no result");
ui->tabWidget->setCurrentWidget(ui->messageTab);
// statusBar()->showMessage(tr("Explain failed."));
}
}
QString QueryTab::getCommand() const
{
QString command;
QTextCursor cursor = ui->queryEdit->textCursor();
if (cursor.hasSelection()) {
command = cursor.selection().toPlainText();
}
else {
command = ui->queryEdit->toPlainText();
}
return command;
}
std::string QueryTab::getCommandUtf8() const
{
return getCommand().toUtf8().data();
}
//QTabWidget *QueryTab::getTabWidget()
//{
// QWidget * w = parentWidget();
// QWidget * p = w->parentWidget();
// QTabWidget *tw = dynamic_cast<QTabWidget*>(p);
// return tw;
//}
//void QueryTab::setTabCaption(const QString &caption, const QString &tooltip)
//{
// QTabWidget *tabwidget = getTabWidget();
// if (tabwidget) {
// int i = tabwidget->indexOf(this);
// if (i >= 0) {
// tabwidget->setTabText(i, caption);
// tabwidget->setTabToolTip(i, tooltip);
// }
// }
//}
void QueryTab::query_ready(Expected<std::shared_ptr<Pgsql::Result>> exp_res, qint64 elapsedms)
{
if (exp_res.valid()) {
auto dbres = exp_res.get();
if (dbres) {
addLog("query_ready with result");
auto st = dbres->resultStatus();
if (st == PGRES_TUPLES_OK) {
//int n_rows = dbres->getRows();
//QString rowcount_str = QString("rows: %1").arg(dbres->getRows());
auto result_model = std::make_shared<QueryResultModel>(nullptr , dbres,
m_catalog);
TuplesResultWidget *trw = new TuplesResultWidget;
trw->setResult(result_model, elapsedms);
resultList.push_back(trw);
ui->tabWidget->addTab(trw, "Data");
if (resultList.size() == 1)
ui->tabWidget->setCurrentWidget(trw);
}
else {
if (st == PGRES_COMMAND_OK) {
int tuples_affected = dbres->tuplesAffected();
QString msg;
if (tuples_affected >= 0)
msg = tr("Query returned succesfully: %1 rows affected, execution time %2")
.arg(QString::number(tuples_affected))
.arg(msfloatToHumanReadableString(elapsedms));
else
msg = tr("Query returned succesfully, execution time %1")
.arg(msfloatToHumanReadableString(elapsedms));
ui->messagesEdit->append(msg);
ui->tabWidget->setCurrentWidget(ui->messageTab);
}
else {
// if (st == PGRES_EMPTY_QUERY) {
// statusBar()->showMessage(tr("Empty query."));
// }
// else if (st == PGRES_COPY_OUT) {
// statusBar()->showMessage(tr("COPY OUT."));
// }
// else if (st == PGRES_COPY_IN) {
// statusBar()->showMessage(tr("COPY IN."));
// }
// else if (st == PGRES_BAD_RESPONSE) {
// statusBar()->showMessage(tr("BAD RESPONSE."));
// }
// else if (st == PGRES_NONFATAL_ERROR) {
// statusBar()->showMessage(tr("NON FATAL ERROR."));
// }
// else if (st == PGRES_FATAL_ERROR) {
// statusBar()->showMessage(tr("FATAL ERROR."));
// }
// else if (st == PGRES_COPY_BOTH) {
// statusBar()->showMessage(tr("COPY BOTH shouldn't happen is for replication."));
// }
// else if (st == PGRES_SINGLE_TUPLE) {
// statusBar()->showMessage(tr("SINGLE TUPLE result."));
// }
// else {
// statusBar()->showMessage(tr("No tuples returned, possibly an error..."));
// }
ui->tabWidget->setCurrentWidget(ui->messageTab);
auto details = dbres->diagDetails();
markError(details);
receiveNotice(details);
}
}
}
else {
m_stopwatch.stop();
addLog("query_ready with NO result");
}
}
else {
// we have an error
}
}
void QueryTab::markError(const Pgsql::ErrorDetails &details)
{
if (details.statementPosition > 0) {
QTextCursor cursor = ui->queryEdit->textCursor();
// Following finds out the start of the current selection
// theoreticallly the selection might have changed however
// theoretically all the text might have changed also so we ignore
// both issues for know and we solve both when we decide it is to much of
// a problem but in practice syntax errors come back very quickly...
int position_offset = 0;
if (cursor.hasSelection()) {
position_offset = cursor.selectionStart();
}
cursor.setPosition(details.statementPosition - 1 + position_offset);
ui->queryEdit->setTextCursor(cursor);
int length = 0;
if (details.state == "42703") {
std::size_t pos = details.messagePrimary.find('"');
if (pos != std::string::npos) {
std::size_t pos2 = details.messagePrimary.find('"', pos+1);
if (pos2 != std::string::npos) {
length = static_cast<int>(pos2 - pos);
}
}
}
else if (details.state == "42P01") {
std::size_t pos = details.messagePrimary.find('"');
if (pos != std::string::npos) {
std::size_t pos2 = details.messagePrimary.find('"', pos+1);
if (pos2 != std::string::npos) {
length = static_cast<int>(pos2 - pos);
}
}
}
ui->queryEdit->addErrorMarker(details.statementPosition - 1 + position_offset, length);
}
}
void QueryTab::clearResult()
{
for (auto e : resultList)
delete e;
resultList.clear();
}
void QueryTab::copyQueryAsCString()
{
// QString command;
// QTextCursor cursor = ui->queryEdit->textCursor();
// if (cursor.hasSelection()) {
// command = cursor.selection().toPlainText();
// }
// else {
// command = ui->queryEdit->toPlainText();
// }
QString command = getCommand();
QString cs = ConvertToMultiLineCString(command);
QApplication::clipboard()->setText(cs);
}
#include <codebuilder/CodeBuilder.h>
#include <codebuilder/DefaultConfigs.h>
void QueryTab::copyQueryAsRawCppString()
{
QString command = getCommand();
//auto sql = getAllOrSelectedSql();
QString cs = ConvertToMultiLineRawCppString(command);
QApplication::clipboard()->setText(cs);
}
void QueryTab::generateCode()
{
QString command = getCommand();
if (resultList.empty()) {
QMessageBox::question(this, "pglab", tr("Please execute the query first"), QMessageBox::Ok);
}
if (resultList.size() == 1) {
std::shared_ptr<const Pgsql::Result> dbres = resultList[0]->GetPgsqlResult();
//context()->newCodeGenPage(command, dbres);
}
}
void QueryTab::exportData(const QString &file_name)
{
auto widget = ui->tabWidget->currentWidget();
auto fi = std::find(resultList.begin(), resultList.end(), widget);
if (fi != resultList.end()) {
TuplesResultWidget* rw = *fi;
rw->exportData(file_name);
}
}
void QueryTab::focusEditor()
{
ui->queryEdit->setFocus();
}
std::vector<QAction*> QueryTab::getToolbarActions()
{
if (actions.empty()) {
QAction *action;
// New
// action = new QAction(QIcon(":/icons/new_query_tab.png"), tr("New"), this);
// action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_N));
// connect(action, &QAction::triggered, this, &QueryTab::);
// actions.push_back(action);
// Load
// Save
action = new QAction(QIcon(":/icons/script_save.png"), tr("Save SQL"), this);
action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
connect(action, &QAction::triggered, this, &QueryTab::save);
actions.push_back(action);
// Save as (menu only)
// action = new QAction(QIcon(":/icons/script_save.png"), tr("Save SQL as"), this);
// //action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
// connect(action, &QAction::triggered, this, &QueryTab::saveAs);
// actions.push_back(action);
// Save copy as
// Copy
// Copy as C-string
// Copy as raw cpp string
// Execute SQL
action = new QAction(QIcon(":/icons/script_go.png"), tr("Execute"), this);
action->setShortcut(QKeySequence(Qt::Key_F5));
connect(action, &QAction::triggered, this, &QueryTab::execute);
actions.push_back(action);
// Explain
action = new QAction(QIcon(":/icons/lightbulb_off.png"), tr("Explain"), this);
action->setShortcut(QKeySequence(Qt::Key_F7));
connect(action, &QAction::triggered, this, [this] () { explain(false); });
actions.push_back(action);
// Explain Anaylze
action = new QAction(QIcon(":/icons/lightbulb.png"), tr("Analyze"), this);
action->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_F7));
connect(action, &QAction::triggered, this, [this] () { explain(true); });
actions.push_back(action);
// Cancel
action = new QAction(QIcon(":/icons/script_delete.png"), tr("Cancel"), this);
action->setShortcut(QKeySequence(Qt::ALT + Qt::Key_Pause));
connect(action, &QAction::triggered, this, &QueryTab::cancel);
actions.push_back(action);
}
return actions;
}
void QueryToolModule::init()
{
auto action_new = new QAction(QIcon(":/icons/new_query_tab.png"), tr("New"), this);
connect(action_new, &QAction::triggered, this, &QueryToolModule::new_triggered);
registerAction(action_new, MenuLocation({"File/New"}), ToolbarLocation("File", ""));
}
void QueryToolModule::new_triggered()
{
}
namespace {
std::shared_ptr<PluginModule> createModule()
{
auto module = std::make_shared<QueryToolModule>("Query tool", "pglab.querytool");
module->init();
PluginRegister::getInstance()->registerModule(module);
return std::move(module);
}
std::weak_ptr<PluginModule> register_variable = createModule();
}