#include "QueryTool.h" #include "ui_QueryTab.h" #include "SqlSyntaxHighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include "ExplainTreeModelItem.h" #include "json/json.h" #include "OpenDatabase.h" #include "catalog/PgDatabaseCatalog.h" #include "QueryParamListController.h" #include "util.h" #include "UserConfiguration.h" #include "IDatabaseWindow.h" QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent) : QWidget(parent) , m_context(context) , ui(new Ui::QueryTab) , m_dbConnection() { 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()); auto types = m_catalog->types(); if (types) { highlighter->setTypes(*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> res, qint64 elapsedms) { if (res.valid()) { auto && dbresult = res.get(); QMetaObject::invokeMethod(this, "query_ready", Q_ARG(std::shared_ptr, dbresult), Q_ARG(qint64, elapsedms)); } else { /// \todo handle error } }; try { if (m_queryParamListController->empty()) m_dbConnection.send(cmd, cb); else m_dbConnection.send(cmd, m_queryParamListController->params(), cb); } catch (const std::exception &ex) { QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); msgBox.setText(QString("Error executing query: %1").arg(QString::fromUtf8(ex.what()))); msgBox.setStandardButtons(QMessageBox::Close); msgBox.setDefaultButton(QMessageBox::Close); msgBox.exec(); } } } 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> 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 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(); } QString QueryTool::title() const { QFileInfo fileInfo(m_fileName); QString fn(fileInfo.fileName()); return fn; } 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: startConnect(); iconname = "red.png"; break; 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 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(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(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(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 #include void QueryTool::copyQueryAsRawCppString() { QString command = getCommand(); QString cs = ConvertToMultiLineRawCppString(command); QApplication::clipboard()->setText(cs); } void QueryTool::pasteLangString() { QString s = QApplication::clipboard()->text(); s = ConvertLangToSqlString(s); ui->queryEdit->insertPlainText(s); } 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 dbres = resultList[0]->GetPgsqlResult(); m_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); }