diff --git a/icons/page_white_add.png b/icons/page_white_add.png new file mode 100644 index 0000000..d33fc2b Binary files /dev/null and b/icons/page_white_add.png differ diff --git a/mainwindow.cpp b/mainwindow.cpp index d3d17bd..feba16f 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,76 +1,22 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -#include "QueryResultModel.h" -#include "QueryExplainModel.h" -#include "sqlhighlighter.h" +//#include "QueryResultModel.h" +//#include "QueryExplainModel.h" #include #include #include -#include -#include #include #include +#include #include -#include "json/json.h" -#include "explaintreemodelitem.h" #include #include #include - +#include "util.h" //#include -namespace { - // Supported range from microseconds to seconds - // min:sec to hours::min::sec - QString msfloatToHumanReadableString(float ms) - { - QString unit; - float val; - int deci = 2; - if (ms < 1.0f) { - val = ms * 1000.f; - //result = QString::asprintf("%0.3f", ms * 1000.0f); - unit = u8"μs"; - } - else if (ms >= 1000.0) { - val = ms / 1000.0f; - unit = "s"; - if (val >= 60.0) { - int secs = val; - int min = secs / 60.0; - secs -= min * 60; - if (min >= 60) { - int hour = min / 60; - min -= hour * 60; - return QString::asprintf("%d:%02d:%02d", hour, min, secs); - } - else { - return QString::asprintf("%02d:%02d", min, secs); - } - } - } - else { - val = ms; - unit = "ms"; - } - -// if (val >= 1000.f) { -// deci = 0; -// } -// else - if (val >= 100.f) { - deci = 0; - } - else if (val >= 10.f) { - deci = 1; - } - QString result = QString::asprintf("%0.*f", deci, val); - return result + unit; - } - -} namespace pg = Pgsql; @@ -89,71 +35,56 @@ const char * test_query = MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) - , m_queryTab(new QueryTab) { ui->setupUi(this); - QFont font; - font.setFamily("Source Code Pro"); - font.setFixedPitch(true); - font.setPointSize(10); - ui->queryEdit->setFont(font); - highlighter.reset(new SqlHighlighter(ui->queryEdit->document())); - -// ui->queryEdit->setPlainText(test_query); -// ui->connectionStringEdit->setText("user=postgres dbname=foutrapport password=admin"); - -// QAction *action; -// action = ui->mainToolBar->addAction("connect"); -// connect(action, &QAction::triggered, this, &MainWindow::startConnect); - -// action = ui->mainToolBar->addAction("explain"); -// connect(action, &QAction::triggered, this, &MainWindow::performExplain); - -// action = ui->mainToolBar->addAction("cancel"); -// connect(action, &QAction::triggered, this, &MainWindow::cancel_query); - - //ui->ResultView->setItemDelegate(); - - m_dbConnection.setStateCallback([this](ASyncDBConnection::State st) - { - QueueTask([this, st]() { connectionStateChanged(st); }); - }); - - m_dbConnection.setNoticeCallback([this](Pgsql::ErrorDetails details) - { - QueueTask([this, details]() { receiveNotice(details); }); - }); - - connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &MainWindow::queryTextChanged); m_timeElapsedLabel = new QLabel(this); statusBar()->addPermanentWidget(m_timeElapsedLabel); - { - QVBoxLayout *layout = new QVBoxLayout; - layout->addWidget(m_queryTab); - layout->setMargin(4); - ui->tab_2->setLayout(layout); - } +// { +// QVBoxLayout *layout = new QVBoxLayout; +// layout->addWidget(m_queryTab); +// layout->setMargin(4); +// ui->tab_2->setLayout(layout); +// } } MainWindow::~MainWindow() { - m_dbConnection.closeConnection(); - m_dbConnection.setStateCallback(nullptr); +// m_dbConnection.closeConnection(); +// m_dbConnection.setStateCallback(nullptr); delete ui; } +void MainWindow::newSqlPage() +{ + QueryTab *qt = new QueryTab(this); + qt->setConfig(m_config); +// QVBoxLayout *layout = new QVBoxLayout; +// layout->addWidget(qt); +// layout->setMargin(4); + + ui->tabWidget->addTab(qt, "Tab"); + +} + +QueryTab *MainWindow::GetActiveQueryTab() +{ + QWidget *widget = ui->tabWidget->currentWidget(); + QueryTab *qt = dynamic_cast(widget); + return qt; +} + void MainWindow::setConfig(const ConnectionConfig &config) { m_config = config; QString title = "pglab - "; title += m_config.name().c_str(); setWindowTitle(title); - QueueTask([this]() { startConnect(); }); + newSqlPage(); } void MainWindow::QueueTask(TSQueue::t_Callable c) @@ -175,255 +106,6 @@ void MainWindow::processCallableQueue() } } -void MainWindow::connectionStateChanged(ASyncDBConnection::State state) -{ - QString status_str; - switch (state) { - case ASyncDBConnection::State::NotConnected: - status_str = tr("Geen verbinding"); - break; - case ASyncDBConnection::State::Connecting: - status_str = tr("Verbinden"); - break; - case ASyncDBConnection::State::Connected: - status_str = tr("Verbonden"); - break; - case ASyncDBConnection::State::QuerySend: - status_str = tr("Query verstuurd"); - break; - case ASyncDBConnection::State::CancelSend: - status_str = tr("Query geannuleerd"); - break; - } - addLog(status_str); - statusBar()->showMessage(status_str); - - bool connected = ASyncDBConnection::State::Connected == state; - ui->actionExecute_SQL->setEnabled(connected); - ui->actionExplain_Analyze->setEnabled(connected); - ui->actionCancel->setEnabled(ASyncDBConnection::State::QuerySend == state); -} - -void MainWindow::startConnect() -{ - m_dbConnection.setupConnection(m_config); -} - -std::string MainWindow::getCommand() const -{ - QString command; - QTextCursor cursor = ui->queryEdit->textCursor(); - if (cursor.hasSelection()) { - command = cursor.selection().toPlainText(); - } - else { - command = ui->queryEdit->toPlainText(); - } - return command.toUtf8().data(); -} - -void MainWindow::performQuery() -{ - if (m_dbConnection.state() == ASyncDBConnection::State::Connected) { - addLog("Query clicked"); - - ui->ResultView->setModel(nullptr); - resultModel.reset(); - ui->messagesEdit->clear(); - - std::string cmd = getCommand(); - startTimer(); - m_dbConnection.send(cmd, - [this](std::shared_ptr res) - { - QueueTask([this, res]() { query_ready(res); }); - }); - } -} - -void MainWindow::query_ready(std::shared_ptr dbres) -{ - endTimer(); - if (dbres) { - addLog("query_ready with result"); - auto st = dbres->resultStatus(); - if (st == PGRES_TUPLES_OK) { - resultModel.reset(new QueryResultModel(nullptr , dbres)); - ui->ResultView->setModel(resultModel.get()); - ui->tabWidget->setCurrentWidget(ui->dataTab); - statusBar()->showMessage(tr("Query ready.")); - } - else { - if (st == PGRES_COMMAND_OK) { - statusBar()->showMessage(tr("Command OK.")); - QString msg = tr("Query returned succesfully: %1 rows affected, %2 execution time.") - .arg(QString::number(dbres->tuplesAffected())) - .arg(msfloatToHumanReadableString(elapsedTime.count())); - 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); - receiveNotice(dbres->diagDetails()); - } - } - } - else { - addLog("query_ready with NO result"); - statusBar()->showMessage(tr("Query cancelled.")); - } -} - -void MainWindow::performExplain() -{ - ui->explainTreeView->setModel(nullptr); - explainModel.reset(); - ui->messagesEdit->clear(); - - addLog("Explain clicked"); - - startTimer(); - std::string cmd = "EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT JSON) " + getCommand(); - m_dbConnection.send(cmd, - [this](std::shared_ptr res) - { - if (res) { - // Process explain data seperately - std::thread([this,res]() - { - std::shared_ptr explain; - if (res->getCols() == 1 && res->getRows() == 1) { - std::string s = res->getVal(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); - } - } - QueueTask([this, explain]() { explain_ready(explain); }); - }).detach(); - } - }); -} - -void MainWindow::explain_ready(ExplainRoot::SPtr explain) -{ - endTimer(); - if (explain) { - addLog("Explain ready"); - QString 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); - statusBar()->showMessage(tr("Explain ready.")); - } - else { - addLog("Explain no result"); - ui->tabWidget->setCurrentWidget(ui->messageTab); - statusBar()->showMessage(tr("Explain failed.")); - } -} - -void MainWindow::cancel_query() -{ - m_dbConnection.cancel(); -} - -void MainWindow::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 MainWindow::addLog(QString s) -{ - QTextCursor text_cursor = QTextCursor(ui->edtLog->document()); - text_cursor.movePosition(QTextCursor::End); - text_cursor.insertText(s + "\r\n"); -} - void MainWindow::startTimer() { m_startTime = std::chrono::steady_clock::now(); @@ -464,83 +146,36 @@ void MainWindow::endTimer() } } -bool MainWindow::continueWithoutSaving() -{ - QMessageBox msgBox; - msgBox.setText("The document has been modified."); - msgBox.setInformativeText("The current query has unsaved changes, do you want to continue without saving those changes?"); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - int ret = msgBox.exec(); - return ret == QMessageBox::Yes; -} - void MainWindow::on_actionLoad_SQL_triggered() { - // - if (m_queryTextChanged && !continueWithoutSaving()) { - return; - } - - QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory); - QString file_name = QFileDialog::getOpenFileName(this, - tr("Open sql query"), home_dir, tr("SQL files (*.sql *.txt)")); - if ( ! file_name.isEmpty()) { - QFile file(file_name); - if (file.open(QIODevice::ReadWrite)) { - QTextStream stream(&file); - ui->queryEdit->clear(); - while (!stream.atEnd()){ - QString line = stream.readLine(); - ui->queryEdit->appendPlainText(line); - } - m_queryTextChanged = false; - m_fileName = file_name; - } - } -} - -void MainWindow::saveSqlTo(const QString &filename) -{ - QFile file(filename); - if (file.open(QIODevice::ReadWrite)) { - QTextStream stream(&file); - QString text = ui->queryEdit->toPlainText(); - stream << text; - m_queryTextChanged = false; + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->open(); } } void MainWindow::on_actionSave_SQL_triggered() { - if (m_fileName.isEmpty()) { - on_actionSave_SQL_as_triggered(); + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->save(); } - else { - saveSqlTo(m_fileName); - } -} - -QString MainWindow::promptUserForSaveSqlFilename() -{ - QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory); - return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)")); } void MainWindow::on_actionSave_SQL_as_triggered() { - QString filename = promptUserForSaveSqlFilename(); - if (!filename.isEmpty()) { - saveSqlTo(filename); - m_fileName = filename; + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->saveAs(); } + } void MainWindow::on_actionSave_copy_of_SQL_as_triggered() { - QString filename = promptUserForSaveSqlFilename(); - if (!filename.isEmpty()) { - saveSqlTo(filename); + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->saveCopyAs(); } } @@ -595,38 +230,44 @@ void Copy( ) void MainWindow::on_actionExecute_SQL_triggered() { - performQuery(); + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->execute(); + } } void MainWindow::on_actionExplain_Analyze_triggered() { - performExplain(); + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->explainAnalyze(); + } } void MainWindow::on_actionCancel_triggered() { - cancel_query(); -} + QueryTab *tab = GetActiveQueryTab(); + if (tab) { + tab->cancel(); + } -void MainWindow::queryTextChanged() -{ - m_queryTextChanged = true; } void MainWindow::closeEvent(QCloseEvent *event) { - if (!m_queryTextChanged || continueWithoutSaving()) { - event->accept(); - } - else { - event->ignore(); - } + // TODO collect which files need saving +// if (!m_queryTextChanged || continueWithoutSaving()) { +// event->accept(); +// } +// else { +// event->ignore(); +// } } void MainWindow::showEvent(QShowEvent *event) { if (!event->spontaneous()) { - m_queryTextChanged = false; +// m_queryTextChanged = false; } event->accept(); } @@ -634,3 +275,8 @@ void MainWindow::showEvent(QShowEvent *event) + +void MainWindow::on_actionNew_SQL_triggered() +{ + newSqlPage(); +} diff --git a/mainwindow.h b/mainwindow.h index b7b9972..424d7ca 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -14,10 +14,9 @@ #include #include -class ExplainRoot; -class QueryResultModel; -class QueryExplainModel; -class SqlHighlighter; +//class ExplainRoot; +//class QueryResultModel; +//class QueryExplainModel; namespace Ui { class MainWindow; @@ -44,55 +43,35 @@ public: */ void QueueTask(TSQueue::t_Callable c); + private: TSQueue m_taskQueue; QLabel *m_timeElapsedLabel; + std::chrono::duration elapsedTime; + + ConnectionConfig m_config; + + std::unique_ptr m_timer; std::chrono::time_point m_startTime; - ConnectionConfig m_config; - bool m_queryTextChanged = false; - QString m_fileName; - - void startTimer(); void endTimer(); - std::chrono::duration elapsedTime; Ui::MainWindow *ui; - QueryTab *m_queryTab = nullptr; - std::unique_ptr highlighter; - ASyncDBConnection m_dbConnection; + QueryTab *GetActiveQueryTab(); - void connectionStateChanged(ASyncDBConnection::State state); - std::unique_ptr resultModel; - std::unique_ptr explainModel; - void query_ready(std::shared_ptr res); - void explain_ready(std::shared_ptr explain); - std::string getCommand() const; - bool continueWithoutSaving(); void closeEvent(QCloseEvent *event); void showEvent(QShowEvent *event); - QString promptUserForSaveSqlFilename(); - void saveSqlTo(const QString &filename); + void newSqlPage(); private slots: - void startConnect(); - - void performQuery(); - void performExplain(); - - void cancel_query(); - void queryTextChanged(); - void receiveNotice(Pgsql::ErrorDetails notice); - void processCallableQueue(); - void addLog(QString s); void updateTimer(); void on_actionLoad_SQL_triggered(); void on_actionSave_SQL_triggered(); @@ -104,6 +83,7 @@ private slots: void on_actionCancel_triggered(); void on_actionSave_SQL_as_triggered(); void on_actionSave_copy_of_SQL_as_triggered(); + void on_actionNew_SQL_triggered(); }; #endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui index c28dacf..d773752 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -31,178 +31,10 @@ 4 - + - 0 + -1 - - - Tab 1 - - - - 4 - - - 4 - - - 4 - - - 4 - - - - - Qt::Vertical - - - - - 1 - - - - Messages - - - - 4 - - - 4 - - - 4 - - - 4 - - - - - true - - - - - - - - Data - - - - 4 - - - 4 - - - 4 - - - 4 - - - - - - Source Sans Pro - 10 - - - - QAbstractItemView::NoEditTriggers - - - true - - - QAbstractItemView::ScrollPerPixel - - - QAbstractItemView::ScrollPerPixel - - - false - - - 20 - - - - - - - - Explain - - - - 2 - - - 2 - - - 2 - - - 2 - - - - - QAbstractItemView::NoEditTriggers - - - false - - - true - - - 10 - - - false - - - false - - - - - - - - - - - - - - - Log - - - - - - - - - - - - - - - Tab 2 - - @@ -220,6 +52,7 @@ File + @@ -244,6 +77,7 @@ false + @@ -344,6 +178,19 @@ Save copy of SQL as + + + + :/icons/page_white_add.png + + + + New SQL + + + Ctrl+N + + diff --git a/pglab.pro b/pglab.pro index 4645c44..af58e7e 100644 --- a/pglab.pro +++ b/pglab.pro @@ -37,7 +37,9 @@ SOURCES += main.cpp\ backuprestore.cpp \ databaseoverviewform.cpp \ dbschema_database.cpp \ - querytab.cpp + querytab.cpp \ + stopwatch.cpp \ + util.cpp HEADERS += mainwindow.h \ serverproperties.h \ @@ -60,7 +62,9 @@ HEADERS += mainwindow.h \ expected.h \ databaseoverviewform.h \ dbschema_database.h \ - querytab.h + querytab.h \ + stopwatch.h \ + util.h FORMS += mainwindow.ui \ serverproperties.ui \ diff --git a/querytab.cpp b/querytab.cpp index b92303e..194814e 100644 --- a/querytab.cpp +++ b/querytab.cpp @@ -1,14 +1,400 @@ -#include "querytab.h" +#include "querytab.h" #include "ui_querytab.h" -QueryTab::QueryTab(QWidget *parent) : +#include "sqlhighlighter.h" + +#include +#include +#include +#include +#include +#include "explaintreemodelitem.h" +#include "json/json.h" +#include "mainwindow.h" +#include "util.h" + +QueryTab::QueryTab(MainWindow *win, QWidget *parent) : QWidget(parent), - ui(new Ui::QueryTab) + ui(new Ui::QueryTab), + m_win(win) { ui->setupUi(this); + + m_dbConnection.setStateCallback([this](ASyncDBConnection::State st) + { + m_win->QueueTask([this, st]() { connectionStateChanged(st); }); + }); + + m_dbConnection.setNoticeCallback([this](Pgsql::ErrorDetails details) + { + m_win->QueueTask([this, details]() { receiveNotice(details); }); + }); + + QFont font; + font.setFamily("Source Code Pro"); + font.setFixedPitch(true); + font.setPointSize(10); + ui->queryEdit->setFont(font); + highlighter.reset(new SqlHighlighter(ui->queryEdit->document())); + + connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTab::queryTextChanged); + } QueryTab::~QueryTab() { + m_dbConnection.closeConnection(); + m_dbConnection.setStateCallback(nullptr); delete ui; } + +void QueryTab::setConfig(const ConnectionConfig &config) +{ + m_config = config; +// QString title = "pglab - "; +// title += m_config.name().c_str(); +// setWindowTitle(title); + m_win->QueueTask([this]() { startConnect(); }); +} + +void QueryTab::open() +{ + if (m_queryTextChanged && !continueWithoutSavingWarning()) { + return; + } + + QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory); + QString file_name = QFileDialog::getOpenFileName(this, + tr("Open sql query"), home_dir, tr("SQL files (*.sql *.txt)")); + if ( ! file_name.isEmpty()) { + QFile file(file_name); + if (file.open(QIODevice::ReadWrite)) { + QTextStream stream(&file); + ui->queryEdit->clear(); + while (!stream.atEnd()){ + QString line = stream.readLine(); + ui->queryEdit->appendPlainText(line); + } + m_queryTextChanged = false; + setFileName(file_name); + } + } +} + +void QueryTab::save() +{ + if (m_fileName.isEmpty()) { + saveAs(); + } + else { + saveSqlTo(m_fileName); + } +} + +void QueryTab::saveAs() +{ + QString filename = promptUserForSaveSqlFilename(); + if (!filename.isEmpty()) { + saveSqlTo(filename); + setFileName(filename); + } +} + +void QueryTab::saveCopyAs() +{ + QString filename = promptUserForSaveSqlFilename(); + if (!filename.isEmpty()) { + saveSqlTo(filename); + } +} + +void QueryTab::execute() +{ + if (m_dbConnection.state() == ASyncDBConnection::State::Connected) { + addLog("Query clicked"); + + ui->ResultView->setModel(nullptr); + resultModel.reset(); + ui->messagesEdit->clear(); + + std::string cmd = getCommand(); + //startTimer(); + m_dbConnection.send(cmd, + [this](std::shared_ptr res) + { + m_win->QueueTask([this, res]() { query_ready(res); }); + }); + } +} + +void QueryTab::explainAnalyze() +{ + ui->explainTreeView->setModel(nullptr); + explainModel.reset(); + ui->messagesEdit->clear(); + + addLog("Explain clicked"); + + //startTimer(); + std::string cmd = "EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT JSON) " + getCommand(); + m_dbConnection.send(cmd, + [this](std::shared_ptr res) + { + if (res) { + // Process explain data seperately + std::thread([this,res]() + { + std::shared_ptr explain; + if (res->getCols() == 1 && res->getRows() == 1) { + std::string s = res->getVal(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); + } + } + m_win->QueueTask([this, explain]() { explain_ready(explain); }); + }).detach(); + } + }); +} + +void QueryTab::cancel() +{ + m_dbConnection.cancel(); +} + + +void QueryTab::setFileName(const QString &filename) +{ + m_fileName = filename; +} + +bool QueryTab::continueWithoutSavingWarning() +{ + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText("The document has been modified."); + msgBox.setInformativeText("The current query has unsaved changes, do you want to continue without saving those changes?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + int ret = msgBox.exec(); + return ret == QMessageBox::Yes; +} + +void QueryTab::saveSqlTo(const QString &filename) +{ + QFile file(filename); + if (file.open(QIODevice::ReadWrite)) { + QTextStream stream(&file); + QString text = ui->queryEdit->toPlainText(); + stream << text; + m_queryTextChanged = false; + } +} + +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 status_str; + switch (state) { + case ASyncDBConnection::State::NotConnected: + status_str = tr("Geen verbinding"); + break; + case ASyncDBConnection::State::Connecting: + status_str = tr("Verbinden"); + break; + case ASyncDBConnection::State::Connected: + status_str = tr("Verbonden"); + break; + case ASyncDBConnection::State::QuerySend: + status_str = tr("Query verstuurd"); + break; + case ASyncDBConnection::State::CancelSend: + status_str = tr("Query geannuleerd"); + break; + } + addLog(status_str); +// statusBar()->showMessage(status_str); + +// bool connected = ASyncDBConnection::State::Connected == state; +// ui->actionExecute_SQL->setEnabled(connected); +// ui->actionExplain_Analyze->setEnabled(connected); +// ui->actionCancel->setEnabled(ASyncDBConnection::State::QuerySend == state); +} + + +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) +{ +// endTimer(); + if (explain) { + addLog("Explain ready"); + QString 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); +// statusBar()->showMessage(tr("Explain ready.")); + } + else { + addLog("Explain no result"); + ui->tabWidget->setCurrentWidget(ui->messageTab); +// statusBar()->showMessage(tr("Explain failed.")); + } +} + +std::string QueryTab::getCommand() const +{ + QString command; + QTextCursor cursor = ui->queryEdit->textCursor(); + if (cursor.hasSelection()) { + command = cursor.selection().toPlainText(); + } + else { + command = ui->queryEdit->toPlainText(); + } + return command.toUtf8().data(); +} + +void QueryTab::query_ready(std::shared_ptr dbres) +{ + //endTimer(); + if (dbres) { + addLog("query_ready with result"); + auto st = dbres->resultStatus(); + if (st == PGRES_TUPLES_OK) { + resultModel.reset(new QueryResultModel(nullptr , dbres)); + ui->ResultView->setModel(resultModel.get()); + ui->tabWidget->setCurrentWidget(ui->dataTab); + //statusBar()->showMessage(tr("Query ready.")); + } + else { + if (st == PGRES_COMMAND_OK) { +// statusBar()->showMessage(tr("Command OK.")); + QString msg = tr("Query returned succesfully: %1 rows affected, %2 execution time.") + .arg(QString::number(dbres->tuplesAffected())) + .arg(0); //msfloatToHumanReadableString(elapsedTime.count())); + 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); + receiveNotice(dbres->diagDetails()); + } + } + } + else { + addLog("query_ready with NO result"); +// statusBar()->showMessage(tr("Query cancelled.")); + } +} diff --git a/querytab.h b/querytab.h index 68db91f..8273bcb 100644 --- a/querytab.h +++ b/querytab.h @@ -1,22 +1,73 @@ -#ifndef QUERYTAB_H +#ifndef QUERYTAB_H #define QUERYTAB_H +#include "asyncdbconnection.h" +#include "QueryResultModel.h" +#include "QueryExplainModel.h" + #include +#include namespace Ui { class QueryTab; } +class MainWindow; +class SqlHighlighter; +class ExplainRoot; +class QueryResultModel; +class QueryExplainModel; + + class QueryTab : public QWidget { Q_OBJECT public: - explicit QueryTab(QWidget *parent = 0); + QueryTab(MainWindow *win, QWidget *parent = 0); ~QueryTab(); + void setConfig(const ConnectionConfig &config); + + void open(); + void save(); + void saveAs(); + void saveCopyAs(); + + void execute(); + void explainAnalyze(); + void cancel(); private: Ui::QueryTab *ui; + MainWindow *m_win; + std::unique_ptr highlighter; + ConnectionConfig m_config; + + QString m_fileName; ///< use setFileName function to set + bool m_queryTextChanged = false; + + void setFileName(const QString &filename); + bool continueWithoutSavingWarning(); + void saveSqlTo(const QString &filename); + QString promptUserForSaveSqlFilename(); + + + ASyncDBConnection m_dbConnection; + std::unique_ptr resultModel; + std::unique_ptr explainModel; + + void addLog(QString s); + + std::string getCommand() const; + void explain_ready(ExplainRoot::SPtr explain); + void query_ready(std::shared_ptr dbres); +private slots: + + void queryTextChanged(); + void connectionStateChanged(ASyncDBConnection::State state); + void receiveNotice(Pgsql::ErrorDetails notice); + + void startConnect(); }; #endif // QUERYTAB_H diff --git a/querytab.ui b/querytab.ui index ac61b75..cbe5677 100644 --- a/querytab.ui +++ b/querytab.ui @@ -37,7 +37,7 @@ - 3 + 0 diff --git a/resources.qrc b/resources.qrc index ad70bd6..4d3c217 100644 --- a/resources.qrc +++ b/resources.qrc @@ -9,5 +9,6 @@ icons/script_save.png icons/lightbulb.png icons/table_save.png + icons/page_white_add.png diff --git a/stopwatch.cpp b/stopwatch.cpp new file mode 100644 index 0000000..d2d33b6 --- /dev/null +++ b/stopwatch.cpp @@ -0,0 +1,38 @@ +#include "stopwatch.h" +#include "util.h" + +StopWatch::StopWatch() + : m_elapsed(std::make_unique()) +{} + +void StopWatch::start() +{ + m_elapsed->start(); + m_timer = std::make_unique(nullptr); + connect(m_timer.get(), SIGNAL(timeout()), this, SLOT(updateTimer())); + m_timer->start(18); +} + +void StopWatch::updateTimer() +{ +// auto nu = std::chrono::steady_clock::now(); +// std::chrono::duration diff = nu - m_startTime; +// elapsedTime = diff; +// m_timeElapsedLabel->setText(msfloatToHumanReadableString(diff.count())); + + qint64 ms = m_elapsed->elapsed(); + msfloatToHumanReadableString(ms); + + if (m_timer) { + int interval = 18; + if (ms >= 10000) { + int rem = ms % 1000; + interval = 1000 - rem; + } + else if (ms >= 1000) { + interval = 100; + } + + m_timer->start(interval); + } +} diff --git a/stopwatch.h b/stopwatch.h new file mode 100644 index 0000000..8506b18 --- /dev/null +++ b/stopwatch.h @@ -0,0 +1,24 @@ +#ifndef STOPWATCH_H +#define STOPWATCH_H + +#include +#include +#include + +class StopWatch : public QObject { + Q_OBJECT +public: + StopWatch(); + + void start(); + qint64 elapsed(); +private: + std::unique_ptr m_elapsed = nullptr; ///< Keeps time + std::unique_ptr m_timer = nullptr; ///< triggers updates + + +private slots: + void updateTimer(); +}; + +#endif // STOPWATCH_H diff --git a/util.cpp b/util.cpp new file mode 100644 index 0000000..22c27f3 --- /dev/null +++ b/util.cpp @@ -0,0 +1,49 @@ +#include "util.h" + +// Supported range from microseconds to seconds +// min:sec to hours::min::sec +QString msfloatToHumanReadableString(float ms) +{ + QString unit; + float val; + int deci = 2; + if (ms < 1.0f) { + val = ms * 1000.f; + //result = QString::asprintf("%0.3f", ms * 1000.0f); + unit = u8"μs"; + } + else if (ms >= 1000.0) { + val = ms / 1000.0f; + unit = "s"; + if (val >= 60.0) { + int secs = val; + int min = secs / 60.0; + secs -= min * 60; + if (min >= 60) { + int hour = min / 60; + min -= hour * 60; + return QString::asprintf("%d:%02d:%02d", hour, min, secs); + } + else { + return QString::asprintf("%02d:%02d", min, secs); + } + } + } + else { + val = ms; + unit = "ms"; + } + +// if (val >= 1000.f) { +// deci = 0; +// } +// else + if (val >= 100.f) { + deci = 0; + } + else if (val >= 10.f) { + deci = 1; + } + QString result = QString::asprintf("%0.*f", deci, val); + return result + unit; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..0a9102d --- /dev/null +++ b/util.h @@ -0,0 +1,8 @@ +#ifndef UTIL_H +#define UTIL_H + +#include + +QString msfloatToHumanReadableString(float ms); + +#endif // UTIL_H