pgLab/mainwindow.cpp
Eelke Klein d19741f111 Connection manager can now open a query window for selected connection.
Query window has now buttons with icons made in the designer for better looks.
Depending on received responses from the database the tabcontrol with the message, data and explain tab
now switches to the appropriate tab.
2017-01-15 21:01:40 +01:00

490 lines
13 KiB
C++

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "QueryResultModel.h"
#include "QueryExplainModel.h"
#include "sqlhighlighter.h"
#include <QStandardPaths>
#include <QFileDialog>
#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 = 3;
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 = 1;
}
else if (val >= 10.f) {
deci = 2;
}
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);
}
void MainWindow::startConnect()
{
// std::string connstr = ui->connectionStringEdit->text().toUtf8().data();
// m_dbConnection.setupConnection(connstr);
m_dbConnection.setupConnection(m_config);
}
void MainWindow::performQuery()
{
addLog("Query clicked");
ui->ResultView->setModel(nullptr);
resultModel.reset();
ui->messagesEdit->clear();
QString command = ui->queryEdit->toPlainText();
std::string cmd = command.toUtf8().data();
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_EMPTY_QUERY) {
statusBar()->showMessage(tr("Empty query."));
}
else if (st == PGRES_COMMAND_OK) {
statusBar()->showMessage(tr("Command OK."));
}
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();
QString command = "EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT JSON) ";
command += ui->queryEdit->toPlainText();
std::string cmd = command.toUtf8().data();
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;
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->append(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();
}