#include "QueryTool.h" #include "ui_QueryTab.h" #include "util/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 "SqlCompletionProvider.h" #include "Pgsql_PgException.h" #include "QueryParamListController.h" #include "util.h" #include "UserConfiguration.h" #include "IDatabaseWindow.h" #include "UnsavedDocumentsDialog.h" #include #include QueryTool::QueryTool(IDatabaseWindow *context, std::shared_ptr document, QWidget *parent) : ManagedPage(parent) , m_context(context) , ui(new Ui::QueryTab) , m_document(std::move(document)) , 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()); ui->queryEdit->setDocument(m_document->textDocument()); m_document->addEditor(this); auto types = m_catalog->types(); if (types) { m_document->highlighter()->setTypes(*types); } connect(m_document.get(), &SqlDocument::modifiedChanged, this, &QueryTool::updateTitle); connect(m_document.get(), &SqlDocument::fileNameChanged, this, &QueryTool::updateTitle); ui->queryEdit->setAcceptDrops(false); setupCompletion(); 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(); } void QueryTool::setupCompletion() { connect(m_catalog.get(), &PgDatabaseCatalog::refreshed, this, [this]() { m_completionProvider.reset(); }); ui->queryEdit->setCompletionCallback([this](const QString &text, int pos) { EditorCompletionResult result; if (!m_catalog) return result; if (!m_completionProvider) m_completionProvider = std::make_shared(buildCompletionCatalog(*m_catalog)); const SqlContext ctx = analyzeSqlContext(text, pos); result.replaceStart = ctx.prefixStart; result.replaceLen = ctx.prefixLen; result.hasQualifier = !ctx.qualifier.isEmpty(); for (const auto &item : m_completionProvider->complete(ctx)) { EditorCompletionItem i; i.display = item.display; i.detail = item.detail; i.insertText = item.insertText; i.ambiguous = item.ambiguous; result.items.push_back(std::move(i)); } return result; }); } QueryTool::~QueryTool() { // Detach the editor from the shared document, it may be destroyed together with this tool auto detached = new QTextDocument(ui->queryEdit); detached->setDocumentLayout(new QPlainTextDocumentLayout(detached)); ui->queryEdit->setDocument(detached); m_document->removeEditor(this); delete ui; } bool QueryTool::CanClose(bool prompt_user) { if (!m_document->isModified()) return true; // The document stays open in another editor, so it is not closed with this tool if (m_document->editors().size() > 1) return true; if (!prompt_user) return false; return confirmClose(this, {m_document}); } bool QueryTool::save() { return m_document->save(this); } bool QueryTool::saveAs() { return m_document->saveAs(this); } void QueryTool::saveCopyAs() { m_document->saveCopyAs(this); } 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) { try { auto dbres = res.get(); QMetaObject::invokeMethod(this, "query_ready", Q_ARG(std::shared_ptr, dbres), Q_ARG(qint64, elapsedms)); } catch (Pgsql::PgException &ex) { //QMessageBox::critical(this, "pglab", QString::fromStdString(ex.what())); QMetaObject::invokeMethod(this, [this, ex] () { QMessageBox::critical(this, "pglab", QString::fromStdString(ex.what())); }); } }; 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_document->fileName()); QString fn(fileInfo.fileName()); return fn; } void QueryTool::updateTitle() { QFileInfo fileInfo(m_document->fileName()); QString title = fileInfo.fileName(); if (m_document->isModified()) { title = "*" + title; } m_context->setTitleForWidget(this, title, m_document->fileName()); } void QueryTool::connectionStateChanged() { QString iconname; switch (m_dbConnection.state()) { case ASyncDBConnection::State::NotConnected: QMessageBox::warning(this, "pglab", tr("Warning connection and any of its session state has been lost")); //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) { m_stopwatch.stop(); addLog("query_ready with NO result"); return; } 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()); TuplesResultWidget *trw = new TuplesResultWidget; trw->setResult(m_context->openDatabase(), dbres, 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 { ui->tabWidget->setCurrentWidget(ui->messageTab); auto details = dbres->diagDetails(); markError(details); receiveNotice(details); } } } 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::StandardButtons(QMessageBox::Ok), 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); }