pgLab/mainwindow.cpp
Eelke Klein fa9787adfd After an insert, update, delete the number of rows affected is reported.
This also makes it clearer the command was executed succesfully.

Times are now printed with no more then two decimals. This prevents confusion
between thousand and decimal seperators.
2017-01-16 18:57:50 +01:00

519 lines
14 KiB
C++

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "QueryResultModel.h"
#include "QueryExplainModel.h"
#include "sqlhighlighter.h"
#include <QStandardPaths>
#include <QFileDialog>
#include <QTextDocumentFragment>
#include <QTextStream>
#include <QTextTable>
#include <QTimer>
#include <windows.h>
#include "json/json.h"
#include "explaintreemodelitem.h"
#include <algorithm>
//#include <thread>
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;
const char * test_query =
//"SELECT id, program, version, lic_bedrijf, lic_plaats, "
//"lic_number, callstack_crc_1, callstack_crc_2, callstack_crc_3, exception_class, "
//"exception_message \nFROM foutrapport"
"SELECT f1.id, f1.program, f1.version, f1.lic_number, f1.callstack_crc_1, f1.callstack_crc_2, array_agg(f2.id) \n"
"FROM foutrapport f1 JOIN foutrapport f2 USING (callstack_crc_2) \n"
"WHERE f1.actief \n"
"GROUP BY f1.id"
;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
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);
m_dbConnection.setStateCallback([this](ASyncDBConnection::State st)
{
QueueTask([this, st]() { connectionStateChanged(st); });
});
m_dbConnection.setNoticeCallback([this](Pgsql::ErrorDetails details)
{
QueueTask([this, details]() { receiveNotice(details); });
});
m_timeElapsedLabel = new QLabel(this);
statusBar()->addPermanentWidget(m_timeElapsedLabel);
}
MainWindow::~MainWindow()
{
m_dbConnection.closeConnection();
m_dbConnection.setStateCallback(nullptr);
delete ui;
}
void MainWindow::setConfig(const ConnectionConfig &config)
{
m_config = config;
QString title = "pglab - ";
title += m_config.name().c_str();
setWindowTitle(title);
QueueTask([this]() { startConnect(); });
}
void MainWindow::QueueTask(TSQueue::t_Callable c)
{
m_taskQueue.add(c);
// Theoretically this needs to be only called if the queue was empty because otherwise it already would
// be busy emptying the queue. For now however I think it is safer to call it just to make sure.
QMetaObject::invokeMethod(this, "processCallableQueue", Qt::QueuedConnection); // queues on main thread
}
void MainWindow::processCallableQueue()
{
if (!m_taskQueue.empty()) {
auto c = m_taskQueue.pop();
c();
if (!m_taskQueue.empty()) {
QTimer::singleShot(0, this, SLOT(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()
{
// std::string connstr = ui->connectionStringEdit->text().toUtf8().data();
// m_dbConnection.setupConnection(connstr);
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<Pgsql::Result> res)
{
QueueTask([this, res]() { query_ready(res); });
});
}
}
void MainWindow::query_ready(std::shared_ptr<Pgsql::Result> 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<Pgsql::Result> res)
{
if (res) {
// Process explain data seperately
std::thread([this,res]()
{
std::shared_ptr<ExplainRoot> 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)
{
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));
}
}
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();
m_timer = std::make_unique<QTimer>(nullptr);
m_timer->setTimerType(Qt::CoarseTimer);
connect(m_timer.get(), SIGNAL(timeout()), this, SLOT(updateTimer()));
m_timer->start(18);
}
void MainWindow::updateTimer()
{
auto nu = std::chrono::steady_clock::now();
std::chrono::duration<float, std::milli> diff = nu - m_startTime;
elapsedTime = diff;
m_timeElapsedLabel->setText(msfloatToHumanReadableString(diff.count()));
if (m_timer) {
int ms = diff.count();
int interval = 18;
if (ms >= 10000) {
int rem = ms % 1000;
interval = 1000 - rem;
}
else if (ms >= 1000) {
interval = 100;
}
m_timer->start(interval);
}
}
void MainWindow::endTimer()
{
if (m_timer) {
m_timer.reset();
updateTimer();
}
}
void MainWindow::on_actionLoad_SQL_triggered()
{
//
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);
}
}
}
}
void MainWindow::on_actionSave_SQL_triggered()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
QString file_name = QFileDialog::getSaveFileName(this,
tr("Save query"), home_dir, tr("SQL file (*.sql)"));
if ( ! file_name.isEmpty()) {
QFile file(file_name);
if (file.open(QIODevice::ReadWrite)) {
QTextStream stream(&file);
QString text = ui->queryEdit->toPlainText();
stream << text;
}
}
}
void MainWindow::on_actionExport_data_triggered()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
QString file_name = QFileDialog::getSaveFileName(this,
tr("Export data"), home_dir, tr("CSV file (*.csv)"));
}
void MainWindow::on_actionClose_triggered()
{
close();
}
void MainWindow::on_actionAbout_triggered()
{
//
}
#if false
void Copy( )
{
QString selected_text;
// You need a pair of indexes to find the row changes
QModelIndex previous = indexes.first();
indexes.removeFirst();
foreach(current, indexes)
{
QVariant data = model->data(current);
QString text = data.toString();
// At this point `text` contains the text in one cell
selected_text.append(text);
// If you are at the start of the row the row number of the previous index
// isn't the same. Text is followed by a row separator, which is a newline.
if (current.row() != previous.row())
{
selected_text.append('\n');
}
// Otherwise it's the same row, so append a column separator, which is a tab.
else
{
selected_text.append('\t');
}
previous = current;
}
QApplication.clipboard().setText(selected_text);
}
#endif
void MainWindow::on_actionExecute_SQL_triggered()
{
performQuery();
}
void MainWindow::on_actionExplain_Analyze_triggered()
{
performExplain();
}
void MainWindow::on_actionCancel_triggered()
{
cancel_query();
}