File indexing completed on 2024-05-12 05:12:48

0001 /*
0002  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
0003  * SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  *
0007  */
0008 
0009 #include "querydebugger.h"
0010 #include "storagedebuggerinterface.h"
0011 #include "ui_querydebugger.h"
0012 #include "ui_queryviewdialog.h"
0013 
0014 #include <KLocalizedString>
0015 
0016 #include <QAbstractListModel>
0017 #include <QDateTime>
0018 #include <QDialog>
0019 #include <QDialogButtonBox>
0020 #include <QFileDialog>
0021 #include <QHeaderView>
0022 #include <QSortFilterProxyModel>
0023 #include <QTableWidget>
0024 
0025 #include <QDBusArgument>
0026 #include <QDBusConnection>
0027 
0028 #include <Akonadi/ControlGui>
0029 #include <Akonadi/ServerManager>
0030 
0031 #include <KColorScheme>
0032 
0033 #include <algorithm>
0034 
0035 Q_DECLARE_METATYPE(QList<QList<QVariant>>)
0036 
0037 QDBusArgument &operator<<(QDBusArgument &arg, const DbConnection &con)
0038 {
0039     arg.beginStructure();
0040     arg << con.id << con.name << con.start << con.trxName << con.transactionStart;
0041     arg.endStructure();
0042     return arg;
0043 }
0044 
0045 const QDBusArgument &operator>>(const QDBusArgument &arg, DbConnection &con)
0046 {
0047     arg.beginStructure();
0048     arg >> con.id >> con.name >> con.start >> con.trxName >> con.transactionStart;
0049     arg.endStructure();
0050     return arg;
0051 }
0052 
0053 struct QueryInfo {
0054     QString query;
0055     quint64 duration;
0056     quint64 calls;
0057 
0058     bool operator<(const QString &other) const
0059     {
0060         return query < other;
0061     }
0062 };
0063 
0064 Q_DECLARE_TYPEINFO(QueryInfo, Q_RELOCATABLE_TYPE);
0065 
0066 class QueryTreeModel : public QAbstractItemModel
0067 {
0068     Q_OBJECT
0069 
0070 public:
0071     enum RowType { Connection, Transaction, Query };
0072 
0073 private:
0074     class Node
0075     {
0076     public:
0077         virtual ~Node() = default;
0078 
0079         Node *parent;
0080         RowType type;
0081         qint64 start;
0082         uint duration;
0083     };
0084 
0085     class QueryNode : public Node
0086     {
0087     public:
0088         QString query;
0089         QString error;
0090         QMap<QString, QVariant> values;
0091         QList<QList<QVariant>> results;
0092         int resultsCount;
0093     };
0094 
0095     class TransactionNode : public QueryNode
0096     {
0097     public:
0098         ~TransactionNode() override
0099         {
0100             qDeleteAll(queries);
0101         }
0102 
0103         enum TransactionType { Begin, Commit, Rollback };
0104         TransactionType transactionType;
0105         QList<QueryNode *> queries;
0106     };
0107 
0108     class ConnectionNode : public Node
0109     {
0110     public:
0111         ~ConnectionNode() override
0112         {
0113             qDeleteAll(queries);
0114         }
0115 
0116         QString name;
0117         QList<Node *> queries; // FIXME: Why can' I use QList<Query*> here??
0118     };
0119 
0120 public:
0121     enum { RowTypeRole = Qt::UserRole + 1, QueryRole, QueryResultsCountRole, QueryResultsRole, QueryValuesRole };
0122 
0123     QueryTreeModel(QObject *parent)
0124         : QAbstractItemModel(parent)
0125     {
0126     }
0127 
0128     ~QueryTreeModel() override
0129     {
0130         qDeleteAll(mConnections);
0131     }
0132 
0133     void clear()
0134     {
0135         beginResetModel();
0136         qDeleteAll(mConnections);
0137         mConnections.clear();
0138         endResetModel();
0139     }
0140 
0141     void addConnection(qint64 id, const QString &name, qint64 timestamp)
0142     {
0143         auto con = new ConnectionNode;
0144         con->parent = nullptr;
0145         con->type = Connection;
0146         con->name = name;
0147         con->start = timestamp;
0148         beginInsertRows(QModelIndex(), mConnections.count(), mConnections.count());
0149         mConnections << con;
0150         mConnectionById.insert(id, con);
0151         endInsertRows();
0152     }
0153 
0154     void updateConnection(qint64 id, const QString &name)
0155     {
0156         auto con = mConnectionById.value(id);
0157         if (!con) {
0158             return;
0159         }
0160 
0161         con->name = name;
0162         const QModelIndex index = createIndex(mConnections.indexOf(con), columnCount() - 1, con);
0163         Q_EMIT dataChanged(index, index.sibling(index.row(), 5));
0164     }
0165 
0166     void addTransaction(qint64 connectionId, const QString &name, qint64 timestamp, uint duration, const QString &error)
0167     {
0168         auto con = mConnectionById.value(connectionId);
0169         if (!con) {
0170             return;
0171         }
0172 
0173         auto trx = new TransactionNode;
0174         trx->query = name;
0175         trx->parent = con;
0176         trx->type = Transaction;
0177         trx->start = timestamp;
0178         trx->duration = duration;
0179         trx->transactionType = TransactionNode::Begin;
0180         trx->error = error.trimmed();
0181         const QModelIndex conIdx = createIndex(mConnections.indexOf(con), 0, con);
0182         beginInsertRows(conIdx, con->queries.count(), con->queries.count());
0183         con->queries << trx;
0184         endInsertRows();
0185     }
0186 
0187     void closeTransaction(qint64 connectionId, bool commit, qint64 timestamp, uint, const QString &error)
0188     {
0189         auto con = mConnectionById.value(connectionId);
0190         if (!con) {
0191             return;
0192         }
0193 
0194         // Find the last open transaction and change it to closed
0195         for (int i = con->queries.count() - 1; i >= 0; i--) {
0196             Node *node = con->queries[i];
0197             if (node->type == Transaction) {
0198                 auto trx = static_cast<TransactionNode *>(node);
0199                 if (trx->transactionType != TransactionNode::Begin) {
0200                     continue;
0201                 }
0202 
0203                 trx->transactionType = commit ? TransactionNode::Commit : TransactionNode::Rollback;
0204                 trx->duration = timestamp - trx->start;
0205                 trx->error = error.trimmed();
0206 
0207                 const QModelIndex trxIdx = createIndex(i, 0, trx);
0208                 Q_EMIT dataChanged(trxIdx, trxIdx.sibling(trxIdx.row(), columnCount() - 1));
0209                 return;
0210             }
0211         }
0212     }
0213 
0214     void addQuery(qint64 connectionId,
0215                   qint64 timestamp,
0216                   uint duration,
0217                   const QString &queryStr,
0218                   const QMap<QString, QVariant> &values,
0219                   int resultsCount,
0220                   const QList<QList<QVariant>> &results,
0221                   const QString &error)
0222     {
0223         auto con = mConnectionById.value(connectionId);
0224         if (!con) {
0225             return;
0226         }
0227 
0228         auto query = new QueryNode;
0229         query->type = Query;
0230         query->start = timestamp;
0231         query->duration = duration;
0232         query->query = queryStr;
0233         query->values = values;
0234         query->resultsCount = resultsCount;
0235         query->results = results;
0236         query->error = error.trimmed();
0237 
0238         if (!con->queries.isEmpty() && con->queries.last()->type == Transaction
0239             && static_cast<TransactionNode *>(con->queries.last())->transactionType == TransactionNode::Begin) {
0240             auto trx = static_cast<TransactionNode *>(con->queries.last());
0241             query->parent = trx;
0242             beginInsertRows(createIndex(con->queries.indexOf(trx), 0, trx), trx->queries.count(), trx->queries.count());
0243             trx->queries << query;
0244             endInsertRows();
0245         } else {
0246             query->parent = con;
0247             beginInsertRows(createIndex(mConnections.indexOf(con), 0, con), con->queries.count(), con->queries.count());
0248             con->queries << query;
0249             endInsertRows();
0250         }
0251     }
0252 
0253     [[nodiscard]] int rowCount(const QModelIndex &parent) const override
0254     {
0255         if (!parent.isValid()) {
0256             return mConnections.count();
0257         }
0258 
0259         Node *node = reinterpret_cast<Node *>(parent.internalPointer());
0260         switch (node->type) {
0261         case Connection:
0262             return static_cast<ConnectionNode *>(node)->queries.count();
0263         case Transaction:
0264             return static_cast<TransactionNode *>(node)->queries.count();
0265         case Query:
0266             return 0;
0267         }
0268 
0269         Q_UNREACHABLE();
0270     }
0271 
0272     [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override
0273     {
0274         Q_UNUSED(parent)
0275         return 5;
0276     }
0277 
0278     [[nodiscard]] QModelIndex parent(const QModelIndex &child) const override
0279     {
0280         if (!child.isValid() || !child.internalPointer()) {
0281             return {};
0282         }
0283 
0284         Node *childNode = reinterpret_cast<Node *>(child.internalPointer());
0285         // childNode is a Connection
0286         if (!childNode->parent) {
0287             return {};
0288         }
0289 
0290         // childNode is a query in transaction
0291         if (childNode->parent->parent) {
0292             auto connection = static_cast<ConnectionNode *>(childNode->parent->parent);
0293             const int trxIdx = connection->queries.indexOf(childNode->parent);
0294             return createIndex(trxIdx, 0, childNode->parent);
0295         } else {
0296             // childNode is a query without transaction or a transaction
0297             return createIndex(mConnections.indexOf(static_cast<ConnectionNode *>(childNode->parent)), 0, childNode->parent);
0298         }
0299     }
0300 
0301     [[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent) const override
0302     {
0303         if (!parent.isValid()) {
0304             if (row < mConnections.count()) {
0305                 return createIndex(row, column, mConnections.at(row));
0306             } else {
0307                 return {};
0308             }
0309         }
0310 
0311         Node *parentNode = reinterpret_cast<Node *>(parent.internalPointer());
0312         switch (parentNode->type) {
0313         case Connection:
0314             if (row < static_cast<ConnectionNode *>(parentNode)->queries.count()) {
0315                 return createIndex(row, column, static_cast<ConnectionNode *>(parentNode)->queries.at(row));
0316             } else {
0317                 return {};
0318             }
0319         case Transaction:
0320             if (row < static_cast<TransactionNode *>(parentNode)->queries.count()) {
0321                 return createIndex(row, column, static_cast<TransactionNode *>(parentNode)->queries.at(row));
0322             } else {
0323                 return {};
0324             }
0325         case Query:
0326             // Query can never have children
0327             return {};
0328         }
0329 
0330         Q_UNREACHABLE();
0331     }
0332 
0333     [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override
0334     {
0335         if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
0336             return {};
0337         }
0338 
0339         switch (section) {
0340         case 0:
0341             return i18n("Name / Query");
0342         case 1:
0343             return i18n("Started");
0344         case 2:
0345             return i18n("Ended");
0346         case 3:
0347             return i18n("Duration");
0348         case 4:
0349             return i18n("Error");
0350         }
0351 
0352         return {};
0353     }
0354 
0355     [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override
0356     {
0357         if (!index.isValid()) {
0358             return {};
0359         }
0360 
0361         Node *node = reinterpret_cast<Node *>(index.internalPointer());
0362         if (role == RowTypeRole) {
0363             return node->type;
0364         } else {
0365             switch (node->type) {
0366             case Connection:
0367                 return connectionData(static_cast<ConnectionNode *>(node), index.column(), role);
0368             case Transaction:
0369                 return transactionData(static_cast<TransactionNode *>(node), index.column(), role);
0370             case Query:
0371                 return queryData(static_cast<QueryNode *>(node), index.column(), role);
0372             }
0373         }
0374 
0375         Q_UNREACHABLE();
0376     }
0377 
0378     void dumpRow(QFile &file, const QModelIndex &idx, int depth)
0379     {
0380         if (idx.isValid()) {
0381             QTextStream stream(&file);
0382             stream << QStringLiteral("  |").repeated(depth) << QLatin1StringView("- ");
0383 
0384             Node *node = reinterpret_cast<Node *>(idx.internalPointer());
0385             switch (node->type) {
0386             case Connection: {
0387                 auto con = static_cast<ConnectionNode *>(node);
0388                 stream << con->name << "    " << fromMSecsSinceEpoch(con->start);
0389                 break;
0390             }
0391             case Transaction: {
0392                 auto trx = static_cast<TransactionNode *>(node);
0393                 stream << idx.data(Qt::DisplayRole).toString() << "    " << fromMSecsSinceEpoch(trx->start);
0394                 if (trx->transactionType > TransactionNode::Begin) {
0395                     stream << " - " << fromMSecsSinceEpoch(trx->start + trx->duration);
0396                 }
0397                 break;
0398             }
0399             case Query: {
0400                 auto query = static_cast<QueryNode *>(node);
0401                 stream << query->query << "    " << fromMSecsSinceEpoch(query->start) << ", took " << query->duration << " ms";
0402                 break;
0403             }
0404             }
0405 
0406             if (node->type >= Transaction) {
0407                 auto query = static_cast<QueryNode *>(node);
0408                 if (!query->error.isEmpty()) {
0409                     stream << '\n' << QStringLiteral("  |").repeated(depth) << QStringLiteral("  Error: ") << query->error;
0410                 }
0411             }
0412 
0413             stream << '\n';
0414         }
0415 
0416         for (int i = 0, c = rowCount(idx); i < c; ++i) {
0417             dumpRow(file, index(i, 0, idx), depth + 1);
0418         }
0419     }
0420 
0421 private:
0422     [[nodiscard]] QString fromMSecsSinceEpoch(qint64 msecs) const
0423     {
0424         return QDateTime::fromMSecsSinceEpoch(msecs).toString(QStringLiteral("dd.MM.yyyy HH:mm:ss.zzz"));
0425     }
0426 
0427     QVariant connectionData(ConnectionNode *connection, int column, int role) const
0428     {
0429         if (role != Qt::DisplayRole) {
0430             return {};
0431         }
0432 
0433         switch (column) {
0434         case 0:
0435             return connection->name;
0436         case 1:
0437             return fromMSecsSinceEpoch(connection->start);
0438         }
0439 
0440         return {};
0441     }
0442 
0443     QVariant transactionData(TransactionNode *transaction, int column, int role) const
0444     {
0445         if (role == Qt::DisplayRole && column == 0) {
0446             QString mode;
0447             switch (transaction->transactionType) {
0448             case TransactionNode::Begin:
0449                 mode = QStringLiteral("BEGIN");
0450                 break;
0451             case TransactionNode::Commit:
0452                 mode = QStringLiteral("COMMIT");
0453                 break;
0454             case TransactionNode::Rollback:
0455                 mode = QStringLiteral("ROLLBACK");
0456                 break;
0457             }
0458             return QStringLiteral("%1 %2").arg(mode, transaction->query);
0459         } else {
0460             return queryData(transaction, column, role);
0461         }
0462     }
0463 
0464     QVariant queryData(QueryNode *query, int column, int role) const
0465     {
0466         switch (role) {
0467         case Qt::BackgroundRole:
0468             if (!query->error.isEmpty()) {
0469                 return KColorScheme(QPalette::Normal).background(KColorScheme::NegativeBackground).color();
0470             }
0471             break;
0472         case Qt::DisplayRole:
0473             switch (column) {
0474             case 0:
0475                 return query->query;
0476             case 1:
0477                 return fromMSecsSinceEpoch(query->start);
0478             case 2:
0479                 return fromMSecsSinceEpoch(query->start + query->duration);
0480             case 3:
0481                 return QTime(0, 0, 0).addMSecs(query->duration).toString(QStringLiteral("HH:mm:ss.zzz"));
0482             case 4:
0483                 return query->error;
0484             }
0485             break;
0486         case QueryRole:
0487             return query->query;
0488         case QueryResultsCountRole:
0489             return query->resultsCount;
0490         case QueryResultsRole:
0491             return QVariant::fromValue(query->results);
0492         case QueryValuesRole:
0493             return query->values;
0494         }
0495 
0496         return {};
0497     }
0498 
0499     QList<ConnectionNode *> mConnections;
0500     QHash<qint64, ConnectionNode *> mConnectionById;
0501 };
0502 
0503 class QueryDebuggerModel : public QAbstractListModel
0504 {
0505     Q_OBJECT
0506 public:
0507     QueryDebuggerModel(QObject *parent)
0508         : QAbstractListModel(parent)
0509     {
0510         mSpecialRows[TOTAL].query = QStringLiteral("TOTAL");
0511         mSpecialRows[TOTAL].duration = 0;
0512         mSpecialRows[TOTAL].calls = 0;
0513     }
0514 
0515     enum SPECIAL_ROWS { TOTAL, NUM_SPECIAL_ROWS };
0516     enum COLUMNS { DurationColumn, CallsColumn, AvgDurationColumn, QueryColumn, NUM_COLUMNS };
0517 
0518     [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override
0519     {
0520         if (orientation == Qt::Vertical || section < 0 || section >= NUM_COLUMNS || (role != Qt::DisplayRole && role != Qt::ToolTipRole)) {
0521             return {};
0522         }
0523 
0524         if (section == QueryColumn) {
0525             return i18n("Query");
0526         } else if (section == DurationColumn) {
0527             return i18n("Duration [ms]");
0528         } else if (section == CallsColumn) {
0529             return i18n("Calls");
0530         } else if (section == AvgDurationColumn) {
0531             return i18n("Avg. Duration [ms]");
0532         }
0533 
0534         return {};
0535     }
0536 
0537     [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
0538     {
0539         if (role != Qt::DisplayRole && role != Qt::ToolTipRole) {
0540             return {};
0541         }
0542 
0543         const int row = index.row();
0544         if (row < 0 || row >= rowCount(index.parent())) {
0545             return {};
0546         }
0547         const int column = index.column();
0548         if (column < 0 || column >= NUM_COLUMNS) {
0549             return {};
0550         }
0551 
0552         const QueryInfo &info = (row < NUM_SPECIAL_ROWS) ? mSpecialRows[row] : mQueries.at(row - NUM_SPECIAL_ROWS);
0553 
0554         if (role == Qt::ToolTipRole) {
0555             return QString(QLatin1StringView("<qt>") + info.query + QLatin1StringView("</qt>"));
0556         }
0557 
0558         if (column == QueryColumn) {
0559             return info.query;
0560         } else if (column == DurationColumn) {
0561             return info.duration;
0562         } else if (column == CallsColumn) {
0563             return info.calls;
0564         } else if (column == AvgDurationColumn) {
0565             return float(info.duration) / info.calls;
0566         }
0567 
0568         return {};
0569     }
0570 
0571     [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override
0572     {
0573         if (!parent.isValid()) {
0574             return mQueries.size() + NUM_SPECIAL_ROWS;
0575         } else {
0576             return 0;
0577         }
0578     }
0579 
0580     [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override
0581     {
0582         if (!parent.isValid()) {
0583             return NUM_COLUMNS;
0584         } else {
0585             return 0;
0586         }
0587     }
0588 
0589     void addQuery(const QString &query, uint duration)
0590     {
0591         QList<QueryInfo>::iterator it = std::lower_bound(mQueries.begin(), mQueries.end(), query);
0592 
0593         const int row = std::distance(mQueries.begin(), it) + NUM_SPECIAL_ROWS;
0594 
0595         if (it != mQueries.end() && it->query == query) {
0596             ++(it->calls);
0597             it->duration += duration;
0598 
0599             Q_EMIT dataChanged(index(row, DurationColumn), index(row, AvgDurationColumn));
0600         } else {
0601             beginInsertRows(QModelIndex(), row, row);
0602             QueryInfo info;
0603             info.query = query;
0604             info.duration = duration;
0605             info.calls = 1;
0606             mQueries.insert(it, info);
0607             endInsertRows();
0608         }
0609 
0610         mSpecialRows[TOTAL].duration += duration;
0611         ++mSpecialRows[TOTAL].calls;
0612         Q_EMIT dataChanged(index(TOTAL, DurationColumn), index(TOTAL, AvgDurationColumn));
0613     }
0614 
0615     void clear()
0616     {
0617         beginResetModel();
0618         mQueries.clear();
0619         mSpecialRows[TOTAL].duration = 0;
0620         mSpecialRows[TOTAL].calls = 0;
0621         endResetModel();
0622     }
0623 
0624 private:
0625     QList<QueryInfo> mQueries;
0626     QueryInfo mSpecialRows[NUM_SPECIAL_ROWS];
0627 };
0628 
0629 class QueryViewDialog : public QDialog
0630 {
0631     Q_OBJECT
0632 public:
0633     QueryViewDialog(const QString &query,
0634                     const QMap<QString, QVariant> &values,
0635                     int resultsCount,
0636                     const QList<QList<QVariant>> &results,
0637                     QWidget *parent = nullptr)
0638         : QDialog(parent)
0639         , mUi(new Ui::QueryViewDialog)
0640     {
0641         mUi->setupUi(this);
0642 
0643         QString q = query;
0644         for (int i = 0; i < values.count(); ++i) {
0645             const int pos = q.indexOf(QLatin1Char('?'));
0646             if (pos == -1) {
0647                 break;
0648             }
0649             q.replace(pos, 1, values.value(QStringLiteral(":%1").arg(i)).toString());
0650         }
0651         mUi->queryLbl->setText(q);
0652         if (!q.startsWith(QLatin1StringView("SELECT"))) {
0653             mUi->resultsLabelLbl->setText(i18n("Affected Rows:"));
0654         }
0655         mUi->resultsLbl->setText(QString::number(resultsCount));
0656 
0657         if (!results.isEmpty()) {
0658             mUi->tableWidget->setRowCount(resultsCount);
0659             const auto &headers = results[0];
0660             mUi->tableWidget->setColumnCount(headers.count());
0661             for (int c = 0; c < headers.count(); ++c) {
0662                 mUi->tableWidget->setHorizontalHeaderItem(c, new QTableWidgetItem(headers[c].toString()));
0663             }
0664             for (int r = 1; r <= resultsCount; ++r) {
0665                 const auto &row = results[r];
0666                 for (int c = 0; c < row.size(); ++c) {
0667                     mUi->tableWidget->setItem(r - 1, c, new QTableWidgetItem(row[c].toString()));
0668                 }
0669             }
0670         }
0671 
0672         connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::accept);
0673     }
0674 
0675 private:
0676     QScopedPointer<Ui::QueryViewDialog> mUi;
0677 };
0678 
0679 QueryDebugger::QueryDebugger(QWidget *parent)
0680     : QWidget(parent)
0681     , mUi(new Ui::QueryDebugger)
0682 {
0683     qDBusRegisterMetaType<QList<QList<QVariant>>>();
0684     qDBusRegisterMetaType<DbConnection>();
0685     qDBusRegisterMetaType<QList<DbConnection>>();
0686 
0687     QString service = QStringLiteral("org.freedesktop.Akonadi");
0688     if (Akonadi::ServerManager::hasInstanceIdentifier()) {
0689         service += QLatin1Char('.') + Akonadi::ServerManager::instanceIdentifier();
0690     }
0691     mDebugger = new org::freedesktop::Akonadi::StorageDebugger(service, QStringLiteral("/storageDebug"), QDBusConnection::sessionBus(), this);
0692 
0693     connect(mDebugger, &OrgFreedesktopAkonadiStorageDebuggerInterface::queryExecuted, this, &QueryDebugger::addQuery);
0694 
0695     mUi->setupUi(this);
0696     connect(mUi->enableDebuggingChkBox, &QAbstractButton::toggled, this, &QueryDebugger::debuggerToggled);
0697 
0698     mQueryList = new QueryDebuggerModel(this);
0699     auto proxy = new QSortFilterProxyModel(this);
0700     proxy->setSourceModel(mQueryList);
0701     proxy->setDynamicSortFilter(true);
0702     mUi->queryListView->setModel(proxy);
0703     mUi->queryListView->header()->setSectionResizeMode(QueryDebuggerModel::CallsColumn, QHeaderView::Fixed);
0704     mUi->queryListView->header()->setSectionResizeMode(QueryDebuggerModel::DurationColumn, QHeaderView::Fixed);
0705     mUi->queryListView->header()->setSectionResizeMode(QueryDebuggerModel::AvgDurationColumn, QHeaderView::Fixed);
0706     mUi->queryListView->header()->setSectionResizeMode(QueryDebuggerModel::QueryColumn, QHeaderView::ResizeToContents);
0707 
0708     connect(mUi->queryTreeView, &QTreeView::doubleClicked, this, &QueryDebugger::queryTreeDoubleClicked);
0709     connect(mUi->saveToFileBtn, &QPushButton::clicked, this, &QueryDebugger::saveTreeToFile);
0710     mQueryTree = new QueryTreeModel(this);
0711     mUi->queryTreeView->setModel(mQueryTree);
0712     connect(mDebugger, &org::freedesktop::Akonadi::StorageDebugger::connectionOpened, mQueryTree, &QueryTreeModel::addConnection);
0713     connect(mDebugger, &org::freedesktop::Akonadi::StorageDebugger::connectionChanged, mQueryTree, &QueryTreeModel::updateConnection);
0714     connect(mDebugger, &org::freedesktop::Akonadi::StorageDebugger::transactionStarted, mQueryTree, &QueryTreeModel::addTransaction);
0715     connect(mDebugger, &org::freedesktop::Akonadi::StorageDebugger::transactionFinished, mQueryTree, &QueryTreeModel::closeTransaction);
0716 
0717     Akonadi::ControlGui::widgetNeedsAkonadi(this);
0718 }
0719 
0720 QueryDebugger::~QueryDebugger()
0721 {
0722     // Disable debugging when turning off Akonadi Console so that we don't waste
0723     // resources on server
0724     mDebugger->enableSQLDebugging(false);
0725 }
0726 
0727 void QueryDebugger::clear()
0728 {
0729     mQueryList->clear();
0730 }
0731 
0732 void QueryDebugger::debuggerToggled(bool on)
0733 {
0734     mDebugger->enableSQLDebugging(on);
0735     if (on) {
0736         mQueryTree->clear();
0737 
0738         const QList<DbConnection> conns = mDebugger->connections();
0739         for (const auto &con : conns) {
0740             mQueryTree->addConnection(con.id, con.name, con.start);
0741             if (con.transactionStart > 0) {
0742                 mQueryTree->addTransaction(con.id, con.trxName, con.transactionStart, 0, QString());
0743             }
0744         }
0745     }
0746 }
0747 
0748 void QueryDebugger::addQuery(double sequence,
0749                              qint64 connectionId,
0750                              qint64 timestamp,
0751                              uint duration,
0752                              const QString &query,
0753                              const QMap<QString, QVariant> &values,
0754                              int resultsCount,
0755                              const QList<QList<QVariant>> &result,
0756                              const QString &error)
0757 {
0758     Q_UNUSED(sequence)
0759     mQueryList->addQuery(query, duration);
0760     mQueryTree->addQuery(connectionId, timestamp, duration, query, values, resultsCount, result, error);
0761 }
0762 
0763 void QueryDebugger::queryTreeDoubleClicked(const QModelIndex &index)
0764 {
0765     if (static_cast<QueryTreeModel::RowType>(index.data(QueryTreeModel::RowTypeRole).toInt()) != QueryTreeModel::Query) {
0766         return;
0767     }
0768 
0769     auto dlg = new QueryViewDialog(index.data(QueryTreeModel::QueryRole).toString(),
0770                                    index.data(QueryTreeModel::QueryValuesRole).value<QMap<QString, QVariant>>(),
0771                                    index.data(QueryTreeModel::QueryResultsCountRole).toInt(),
0772                                    index.data(QueryTreeModel::QueryResultsRole).value<QList<QList<QVariant>>>(),
0773                                    this);
0774     connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
0775     dlg->show();
0776 }
0777 
0778 void QueryDebugger::saveTreeToFile()
0779 {
0780     const QString fileName = QFileDialog::getSaveFileName(this);
0781     if (fileName.isEmpty()) {
0782         return;
0783     }
0784     QFile file(fileName);
0785     if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
0786         // show error
0787         return;
0788     }
0789 
0790     mQueryTree->dumpRow(file, QModelIndex(), 0);
0791 
0792     file.close();
0793 }
0794 
0795 #include "querydebugger.moc"
0796 
0797 #include "moc_querydebugger.cpp"