Skip to content

Commit ef9acdb

Browse files
Merge pull request #118 from christianhelle/feature/query-sorting-freeze-fixes-4530f6
Open query results in their own order instead of sorting them backwards
2 parents 147ee51 + 68c827a commit ef9acdb

2 files changed

Lines changed: 149 additions & 4 deletions

File tree

src/gui/queryresultpresenter.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@
55
#include <QItemSelectionModel>
66
#include <QPointer>
77
#include <QAbstractItemModel>
8+
#include <QHeaderView>
9+
10+
namespace {
11+
// Qt starts a header off pointing at the first column in descending order,
12+
// and enabling sorting on a view sorts whatever model is bound to it there
13+
// and then. A paged result answers a sort by re-running its statement
14+
// wrapped in an ORDER BY, so every result used to be executed twice and
15+
// shown backwards before anyone had asked for an order -- and the wrap
16+
// undoes the paging, because the database has to order the whole result set
17+
// before it can hand back the first page.
18+
//
19+
// Clearing the indicator first leaves the rows in the order the statement
20+
// produced them and makes a sort request for column -1, which the paged
21+
// model ignores. The first click on any column then sorts ascending, which
22+
// is Qt's default order for a section the indicator is not already on.
23+
void enableSortingAscending(QTableView *view) {
24+
view->horizontalHeader()->setSortIndicator(-1, Qt::AscendingOrder);
25+
view->setSortingEnabled(true);
26+
}
27+
}
828

929
QueryResultPresenter::QueryResultPresenter(QWidget *parent)
1030
: widget(parent) {
@@ -49,8 +69,10 @@ void QueryResultPresenter::present(const QList<QAbstractItemModel *> &models) {
4969

5070
// Parent the model to its view so it is destroyed along with the view.
5171
model->setParent(tablePtr);
72+
// Sorting is enabled before the model is bound, so binding it does not
73+
// sort it.
74+
enableSortingAscending(tablePtr);
5275
tablePtr->setModel(model);
53-
tablePtr->setSortingEnabled(true);
5476
tablePtr->setGeometry(QRect(0, yOffset, width, height));
5577
tablePtr->show();
5678
this->tableViews.append(tablePtr);
@@ -73,8 +95,10 @@ void QueryResultPresenter::presentToView(QTableView *view, QAbstractItemModel *m
7395
const QPointer<QItemSelectionModel> previousSelection = view->selectionModel();
7496

7597
model->setParent(view);
98+
// As in present(): sorting is set up while the outgoing model is still
99+
// bound, because doing it afterwards would sort the table on the way in.
100+
enableSortingAscending(view);
76101
view->setModel(model);
77102
delete previousSelection.data();
78103
delete previousModel.data();
79-
view->setSortingEnabled(true);
80104
}

tests/test_queryresultpresenter.cpp

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,74 @@
11
#include <gtest/gtest.h>
22
#include <QAbstractItemModel>
3+
#include <QApplication>
4+
#include <QHeaderView>
5+
#include <QMouseEvent>
36
#include <QStandardItemModel>
47
#include <QTableView>
58
#include <QWidget>
9+
#include <algorithm>
10+
#include <memory>
611

712
#include "gui/queryresultpresenter.h"
813

14+
namespace {
15+
// What a view asked a model to sort by. A paged result answers a sort by
16+
// re-running its statement, so an unasked-for sort is both the wrong order
17+
// and a second execution of the query.
18+
//
19+
// The record is kept outside the model and shared with it, because binding
20+
// a model to a view deletes the one it replaces: a test that reads this
21+
// after the next result arrives would otherwise be reading freed memory.
22+
struct SortLog {
23+
QList<int> columns{};
24+
QList<Qt::SortOrder> orders{};
25+
26+
// Sorting by column -1 is Qt's "leave it in its natural order", so it
27+
// does not count as having sorted anything.
28+
[[nodiscard]] bool sorted() const {
29+
return std::any_of(columns.begin(),
30+
columns.end(),
31+
[](const int column) { return column >= 0; });
32+
}
33+
};
34+
35+
class SortSpyModel final : public QStandardItemModel {
36+
public:
37+
SortSpyModel(const int rows, const int columns, std::shared_ptr<SortLog> log)
38+
: QStandardItemModel(rows, columns), log(std::move(log)) {
39+
}
40+
41+
void sort(const int column, const Qt::SortOrder order) override {
42+
if (this->log != nullptr) {
43+
this->log->columns.append(column);
44+
this->log->orders.append(order);
45+
}
46+
// Column -1 is Qt asking for no particular order, which is not
47+
// something to hand to a model that orders rows for real.
48+
if (column >= 0)
49+
QStandardItemModel::sort(column, order);
50+
}
51+
52+
private:
53+
std::shared_ptr<SortLog> log;
54+
};
55+
56+
void clickHeaderSection(const QTableView *view, const int section) {
57+
const auto *header = view->horizontalHeader();
58+
const QPoint position(
59+
header->sectionViewportPosition(section) + header->sectionSize(section) / 2,
60+
header->viewport()->height() / 2);
61+
const QPoint global = header->viewport()->mapToGlobal(position);
62+
QMouseEvent press(QEvent::MouseButtonPress, position, global,
63+
Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
64+
// A release reports the buttons still held afterwards, which is none.
65+
QMouseEvent release(QEvent::MouseButtonRelease, position, global,
66+
Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
67+
QApplication::sendEvent(header->viewport(), &press);
68+
QApplication::sendEvent(header->viewport(), &release);
69+
}
70+
}
71+
972
class QueryResultPresenterTest : public ::testing::Test {
1073
protected:
1174
void SetUp() override {
@@ -16,8 +79,9 @@ class QueryResultPresenterTest : public ::testing::Test {
1679
}
1780

1881
// Stands in for a PagedResult; the presenter only ever binds models.
19-
static QAbstractItemModel *modelWithRows(const int rows) {
20-
auto *model = new QStandardItemModel(rows, 2);
82+
static SortSpyModel *modelWithRows(const int rows,
83+
std::shared_ptr<SortLog> log = nullptr) {
84+
auto *model = new SortSpyModel(rows, 2, std::move(log));
2185
for (int row = 0; row < rows; ++row) {
2286
model->setItem(row, 0, new QStandardItem(QString::number(row)));
2387
model->setItem(row, 1, new QStandardItem("name" + QString::number(row)));
@@ -97,3 +161,60 @@ TEST_F(QueryResultPresenterTest, PresentToViewIgnoresMissingModel) {
97161
ASSERT_NE(view.model(), nullptr) << "a failed preview cleared the view";
98162
EXPECT_EQ(view.model()->rowCount(), 3);
99163
}
164+
165+
// Qt's header starts out on the first column in descending order, and enabling
166+
// sorting sorts the bound model straight away. A paged result answers that by
167+
// re-running its statement wrapped in an ORDER BY, which both reverses the rows
168+
// and makes the database order the whole result set before the first page can
169+
// be shown.
170+
TEST_F(QueryResultPresenterTest, PresentDoesNotSortTheResult) {
171+
const auto log = std::make_shared<SortLog>();
172+
173+
presenter->present({modelWithRows(3, log)});
174+
175+
EXPECT_FALSE(log->sorted()) << "the result was sorted before anyone asked for an order";
176+
177+
const auto views = parent->findChildren<QTableView *>();
178+
ASSERT_EQ(views.size(), 1);
179+
EXPECT_EQ(views.at(0)->horizontalHeader()->sortIndicatorSection(), -1)
180+
<< "the result opened claiming to be sorted by a column";
181+
}
182+
183+
TEST_F(QueryResultPresenterTest, PresentToViewDoesNotSortTheTable) {
184+
QTableView view(parent.get());
185+
186+
const auto first = std::make_shared<SortLog>();
187+
const auto second = std::make_shared<SortLog>();
188+
189+
presenter->presentToView(&view, modelWithRows(3, first));
190+
presenter->presentToView(&view, modelWithRows(4, second));
191+
192+
EXPECT_FALSE(first->sorted()) << "the outgoing table was sorted on its way out";
193+
EXPECT_FALSE(second->sorted()) << "the table was sorted before anyone asked for an order";
194+
}
195+
196+
TEST_F(QueryResultPresenterTest, FirstClickOnAColumnSortsAscending) {
197+
const auto log = std::make_shared<SortLog>();
198+
presenter->present({modelWithRows(3, log)});
199+
const auto views = parent->findChildren<QTableView *>();
200+
ASSERT_EQ(views.size(), 1);
201+
202+
clickHeaderSection(views.at(0), 0);
203+
204+
ASSERT_FALSE(log->orders.isEmpty()) << "clicking the header sorted nothing";
205+
EXPECT_EQ(log->columns.last(), 0);
206+
EXPECT_EQ(log->orders.last(), Qt::AscendingOrder);
207+
}
208+
209+
TEST_F(QueryResultPresenterTest, SecondClickOnTheSameColumnSortsDescending) {
210+
const auto log = std::make_shared<SortLog>();
211+
presenter->present({modelWithRows(3, log)});
212+
const auto views = parent->findChildren<QTableView *>();
213+
ASSERT_EQ(views.size(), 1);
214+
215+
clickHeaderSection(views.at(0), 0);
216+
clickHeaderSection(views.at(0), 0);
217+
218+
ASSERT_FALSE(log->orders.isEmpty()) << "clicking the header sorted nothing";
219+
EXPECT_EQ(log->orders.last(), Qt::DescendingOrder);
220+
}

0 commit comments

Comments
 (0)