pgLab/pglab/QueryTool.cpp
eelke 1a2ec6a224 DatabaseWindow now provides some functionality to its child components through the IDatabaseWindow interface.
This way children do not need to include the full header to get access to some utility functions for changing
the titles and icons of tabpages (and in fact do not need to know that there are tabs, could be something else)
2019-08-16 08:29:27 +02:00

596 lines
16 KiB
C++

#include "QueryTool.h"
#include "ui_QueryTab.h"
#include "SqlSyntaxHighlighter.h"
#include <QStandardPaths>
#include <QPushButton>
#include <QAction>
#include <QFileDialog>
#include <QMessageBox>
#include <QStatusBar>
#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 "IDatabaseWindow.h"
QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
: QWidget(parent)
, m_context(context)
, ui(new Ui::QueryTab)
, m_dbConnection(*getGlobalAsioIoService())
{
ui->setupUi(this);
auto db = context->openDatabase();
m_config = db->config();
m_catalog = db->catalog();
connect(&m_dbConnection, &ASyncDBConnection::onStateChanged, this, &QueryTool::connectionStateChanged);
connect(&m_dbConnection, &ASyncDBConnection::onNotice, this, &QueryTool::receiveNotice);
ui->queryEdit->setFont(UserConfiguration::instance()->codeFont());
highlighter = new SqlSyntaxHighlighter(ui->queryEdit->document());
highlighter->setTypes(*m_catalog->types());
connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTool::queryTextChanged);
m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this);
connect(ui->addButton, &QPushButton::clicked, m_queryParamListController,
&QueryParamListController::on_addParam);
connect(ui->removeButton, &QPushButton::clicked, m_queryParamListController,
&QueryParamListController::on_removeParam);
startConnect();
}
QueryTool::~QueryTool()
{
delete ui;
}
bool QueryTool::canClose()
{
bool can_close;
if (m_queryTextChanged) {
can_close = continueWithoutSavingWarning();
}
else {
can_close = true;
}
return can_close;
}
void QueryTool::newdoc()
{
ui->queryEdit->clear();
setFileName(tr("new"));
m_queryTextChanged = false;
m_new = true;
}
bool QueryTool::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 QueryTool::save()
{
bool result;
if (m_fileName.isEmpty() || m_new) {
result = saveAs();
}
else {
result = saveSqlTo(m_fileName);
}
return result;
}
bool QueryTool::saveAs()
{
bool result = false;
QString filename = promptUserForSaveSqlFilename();
if (!filename.isEmpty()) {
result = saveSqlTo(filename);
if (result) {
setFileName(filename);
m_new = false;
}
}
return result;
}
void QueryTool::saveCopyAs()
{
QString filename = promptUserForSaveSqlFilename();
if (!filename.isEmpty()) {
saveSqlTo(filename);
}
}
void QueryTool::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)
{
if (res.valid()) {
auto && dbresult = res.get();
QMetaObject::invokeMethod(this, "query_ready",
Q_ARG(std::shared_ptr<Pgsql::Result>, dbresult),
Q_ARG(qint64, elapsedms));
}
else {
/// \todo handle error
}
};
if (m_queryParamListController->empty())
m_dbConnection.send(cmd, cb);
else
m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
}
}
void QueryTool::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);
}
}
QMetaObject::invokeMethod(this, "explain_ready",
Q_ARG(ExplainRoot::SPtr, explain));
}).detach();
}
}
};
if (m_queryParamListController->empty())
m_dbConnection.send(cmd, cb);
else
m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
}
void QueryTool::cancel()
{
m_dbConnection.cancel();
}
void QueryTool::setFileName(const QString &filename)
{
m_fileName = filename;
QFileInfo fileInfo(filename);
QString fn(fileInfo.fileName());
m_context->setTitleForWidget(this, fn, m_fileName);
}
bool QueryTool::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 QueryTool::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 QueryTool::promptUserForSaveSqlFilename()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)"));
}
void QueryTool::queryTextChanged()
{
m_queryTextChanged = true;
}
void QueryTool::connectionStateChanged(ASyncDBConnection::State state)
{
QString iconname;
switch (state) {
case ASyncDBConnection::State::NotConnected:
case ASyncDBConnection::State::Connecting:
iconname = "red.png";
break;
case ASyncDBConnection::State::Connected:
iconname = "green.png";
break;
case ASyncDBConnection::State::QuerySend:
case ASyncDBConnection::State::CancelSend:
iconname = "yellow.png";
break;
case ASyncDBConnection::State::Terminating:
break;
}
m_context->setIconForWidget(this, QIcon(":/icons/16x16/document_" + iconname));
}
void QueryTool::addLog(QString s)
{
QTextCursor text_cursor = QTextCursor(ui->edtLog->document());
text_cursor.movePosition(QTextCursor::End);
text_cursor.insertText(s + "\r\n");
}
void QueryTool::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 QueryTool::startConnect()
{
m_dbConnection.setupConnection(m_config);
}
void QueryTool::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);
m_context->showStatusBarMessage(tr("Explain ready."));
}
else {
addLog(tr("Explain no result"));
ui->tabWidget->setCurrentWidget(ui->messageTab);
m_context->showStatusBarMessage(tr("Explain no result"));
}
}
QString QueryTool::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 QueryTool::getCommandUtf8() const
{
return getCommand().toUtf8().data();
}
void QueryTool::query_ready(std::shared_ptr<Pgsql::Result> dbres, qint64 elapsedms)
{
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");
}
}
void QueryTool::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 QueryTool::clearResult()
{
for (auto e : resultList)
delete e;
resultList.clear();
}
void QueryTool::copyQueryAsCString()
{
QString command = getCommand();
QString cs = ConvertToMultiLineCString(command);
QApplication::clipboard()->setText(cs);
}
#include <codebuilder/CodeBuilder.h>
#include <codebuilder/DefaultConfigs.h>
void QueryTool::copyQueryAsRawCppString()
{
QString command = getCommand();
QString cs = ConvertToMultiLineRawCppString(command);
QApplication::clipboard()->setText(cs);
}
void QueryTool::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 QueryTool::exportDataToFilename(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 QueryTool::focusEditor()
{
ui->queryEdit->setFocus();
}
void QueryTool::exportData()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
QString file_name = QFileDialog::getSaveFileName(this,
tr("Export data"), home_dir, tr("CSV file (*.csv)"));
exportDataToFilename(file_name);
}