diff options
author | Christian Mollekopf <chrigi_1@fastmail.fm> | 2016-02-20 20:49:17 +0100 |
---|---|---|
committer | Christian Mollekopf <chrigi_1@fastmail.fm> | 2016-02-20 20:49:17 +0100 |
commit | ed42bdd74d70c7bcb9e1fb8f071ccb92b1515406 (patch) | |
tree | fc068b2f678d3d38d5ad398e6c85474306a0aa02 | |
parent | bc06643cd0c16140f6013be35b64732c1676e794 (diff) | |
download | sink-ed42bdd74d70c7bcb9e1fb8f071ccb92b1515406.tar.gz sink-ed42bdd74d70c7bcb9e1fb8f071ccb92b1515406.zip |
Fetch more data on demand
We skip values we've already seen and only retrieve the new ones.
This currently only properly works in a non-live query and we don't
give the model any feedback when we can't fetch more data anymore.
However, it generally works and we get the desired effect.
-rw-r--r-- | common/CMakeLists.txt | 1 | ||||
-rw-r--r-- | common/modelresult.cpp | 4 | ||||
-rw-r--r-- | common/queryrunner.cpp | 13 | ||||
-rw-r--r-- | common/resultset.cpp | 134 | ||||
-rw-r--r-- | common/resultset.h | 101 | ||||
-rw-r--r-- | common/store.cpp | 3 | ||||
-rw-r--r-- | common/typeindex.cpp | 4 | ||||
-rw-r--r-- | tests/querytest.cpp | 8 |
8 files changed, 171 insertions, 97 deletions
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index fe72605..0d61d90 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt | |||
@@ -58,6 +58,7 @@ set(command_SRCS | |||
58 | typeindex.cpp | 58 | typeindex.cpp |
59 | resourcefacade.cpp | 59 | resourcefacade.cpp |
60 | resourceconfig.cpp | 60 | resourceconfig.cpp |
61 | resultset.cpp | ||
61 | domain/applicationdomaintype.cpp | 62 | domain/applicationdomaintype.cpp |
62 | domain/event.cpp | 63 | domain/event.cpp |
63 | domain/mail.cpp | 64 | domain/mail.cpp |
diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 5b5a473..ceefa76 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp | |||
@@ -155,7 +155,8 @@ bool ModelResult<T, Ptr>::hasChildren(const QModelIndex &parent) const | |||
155 | template<class T, class Ptr> | 155 | template<class T, class Ptr> |
156 | bool ModelResult<T, Ptr>::canFetchMore(const QModelIndex &parent) const | 156 | bool ModelResult<T, Ptr>::canFetchMore(const QModelIndex &parent) const |
157 | { | 157 | { |
158 | return !mEntityChildrenFetched.contains(parent.internalId()); | 158 | const auto id = parent.internalId(); |
159 | return !mEntityChildrenFetched.contains(id) || mEntityChildrenFetchComplete.contains(id); | ||
159 | } | 160 | } |
160 | 161 | ||
161 | template<class T, class Ptr> | 162 | template<class T, class Ptr> |
@@ -218,6 +219,7 @@ template<class T, class Ptr> | |||
218 | void ModelResult<T, Ptr>::fetchEntities(const QModelIndex &parent) | 219 | void ModelResult<T, Ptr>::fetchEntities(const QModelIndex &parent) |
219 | { | 220 | { |
220 | const auto id = getIdentifier(parent); | 221 | const auto id = getIdentifier(parent); |
222 | mEntityChildrenFetchComplete.remove(id); | ||
221 | mEntityChildrenFetched.insert(id); | 223 | mEntityChildrenFetched.insert(id); |
222 | Trace() << "Loading child entities of parent " << id; | 224 | Trace() << "Loading child entities of parent " << id; |
223 | if (loadEntities) { | 225 | if (loadEntities) { |
diff --git a/common/queryrunner.cpp b/common/queryrunner.cpp index 2f627bf..5ac1344 100644 --- a/common/queryrunner.cpp +++ b/common/queryrunner.cpp | |||
@@ -79,9 +79,12 @@ QueryRunner<DomainType>::QueryRunner(const Sink::Query &query, const Sink::Resou | |||
79 | mBatchSize(query.limit) | 79 | mBatchSize(query.limit) |
80 | { | 80 | { |
81 | Trace() << "Starting query"; | 81 | Trace() << "Starting query"; |
82 | if (query.limit && query.sortProperty.isEmpty()) { | ||
83 | Warning() << "A limited query without sorting is typically a bad idea."; | ||
84 | } | ||
82 | //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. | 85 | //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. |
83 | mResultProvider->setFetcher([=](const typename DomainType::Ptr &parent) { | 86 | mResultProvider->setFetcher([=](const typename DomainType::Ptr &parent) { |
84 | Trace() << "Running fetcher"; | 87 | Trace() << "Running fetcher. Offset: " << mOffset << " Batchsize: " << mBatchSize; |
85 | auto resultProvider = mResultProvider; | 88 | auto resultProvider = mResultProvider; |
86 | async::run<qint64>([=]() -> qint64 { | 89 | async::run<qint64>([=]() -> qint64 { |
87 | QueryWorker<DomainType> worker(query, instanceIdentifier, factory, bufferType, mResultTransformation); | 90 | QueryWorker<DomainType> worker(query, instanceIdentifier, factory, bufferType, mResultTransformation); |
@@ -89,6 +92,7 @@ QueryRunner<DomainType>::QueryRunner(const Sink::Query &query, const Sink::Resou | |||
89 | return newRevision; | 92 | return newRevision; |
90 | }) | 93 | }) |
91 | .template then<void, qint64>([query, this](qint64 newRevision) { | 94 | .template then<void, qint64>([query, this](qint64 newRevision) { |
95 | mOffset += mBatchSize; | ||
92 | //Only send the revision replayed information if we're connected to the resource, there's no need to start the resource otherwise. | 96 | //Only send the revision replayed information if we're connected to the resource, there's no need to start the resource otherwise. |
93 | if (query.liveQuery) { | 97 | if (query.liveQuery) { |
94 | mResourceAccess->sendRevisionReplayedCommand(newRevision); | 98 | mResourceAccess->sendRevisionReplayedCommand(newRevision); |
@@ -325,7 +329,9 @@ ResultSet QueryWorker<DomainType>::filterAndSortSet(ResultSet &resultSet, const | |||
325 | }; | 329 | }; |
326 | 330 | ||
327 | auto skip = [iterator]() { | 331 | auto skip = [iterator]() { |
328 | iterator->next(); | 332 | if (iterator->hasNext()) { |
333 | iterator->next(); | ||
334 | } | ||
329 | }; | 335 | }; |
330 | return ResultSet(generator, skip); | 336 | return ResultSet(generator, skip); |
331 | } else { | 337 | } else { |
@@ -339,8 +345,7 @@ ResultSet QueryWorker<DomainType>::filterAndSortSet(ResultSet &resultSet, const | |||
339 | if ((operation != Sink::Operation_Removal) && filter(domainObject)) { | 345 | if ((operation != Sink::Operation_Removal) && filter(domainObject)) { |
340 | //In the initial set every entity is new | 346 | //In the initial set every entity is new |
341 | callback(domainObject, Sink::Operation_Creation); | 347 | callback(domainObject, Sink::Operation_Creation); |
342 | } | 348 | } } else { |
343 | } else { | ||
344 | //Always remove removals, they probably don't match due to non-available properties | 349 | //Always remove removals, they probably don't match due to non-available properties |
345 | if ((operation == Sink::Operation_Removal) || filter(domainObject)) { | 350 | if ((operation == Sink::Operation_Removal) || filter(domainObject)) { |
346 | //TODO only replay if this is in the currently visible set (or just always replay, worst case we have a couple to many results) | 351 | //TODO only replay if this is in the currently visible set (or just always replay, worst case we have a couple to many results) |
diff --git a/common/resultset.cpp b/common/resultset.cpp new file mode 100644 index 0000000..6e1479a --- /dev/null +++ b/common/resultset.cpp | |||
@@ -0,0 +1,134 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2016 Christian Mollekopf <chrigi_1@fastmail.fm> | ||
3 | * | ||
4 | * This program is free software; you can redistribute it and/or modify | ||
5 | * it under the terms of the GNU General Public License as published by | ||
6 | * the Free Software Foundation; either version 2 of the License, or | ||
7 | * (at your option) any later version. | ||
8 | * | ||
9 | * This program is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | * GNU General Public License for more details. | ||
13 | * | ||
14 | * You should have received a copy of the GNU General Public License | ||
15 | * along with this program; if not, write to the | ||
16 | * Free Software Foundation, Inc., | ||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
18 | */ | ||
19 | #include "resultset.h" | ||
20 | |||
21 | #include "common/log.h" | ||
22 | |||
23 | ResultSet::ResultSet() | ||
24 | : mIt(nullptr) | ||
25 | { | ||
26 | |||
27 | } | ||
28 | |||
29 | ResultSet::ResultSet(const ValueGenerator &generator, const SkipValue &skip) | ||
30 | : mIt(nullptr), | ||
31 | mValueGenerator(generator), | ||
32 | mSkip(skip) | ||
33 | { | ||
34 | |||
35 | } | ||
36 | |||
37 | ResultSet::ResultSet(const IdGenerator &generator) | ||
38 | : mIt(nullptr), | ||
39 | mGenerator(generator), | ||
40 | mSkip([this]() { | ||
41 | next(); | ||
42 | }) | ||
43 | { | ||
44 | |||
45 | } | ||
46 | |||
47 | ResultSet::ResultSet(const QVector<QByteArray> &resultSet) | ||
48 | : mResultSet(resultSet), | ||
49 | mIt(mResultSet.constBegin()), | ||
50 | mSkip([this]() { | ||
51 | if (mIt != mResultSet.constEnd()) { | ||
52 | mIt++; | ||
53 | } | ||
54 | }), | ||
55 | mFirst(true) | ||
56 | { | ||
57 | |||
58 | } | ||
59 | |||
60 | ResultSet::ResultSet(const ResultSet &other) | ||
61 | : mResultSet(other.mResultSet), | ||
62 | mIt(nullptr), | ||
63 | mFirst(true) | ||
64 | { | ||
65 | if (other.mValueGenerator) { | ||
66 | mValueGenerator = other.mValueGenerator; | ||
67 | mSkip = other.mSkip; | ||
68 | } else if (other.mGenerator) { | ||
69 | mGenerator = other.mGenerator; | ||
70 | mSkip = [this]() { | ||
71 | next(); | ||
72 | }; | ||
73 | } else { | ||
74 | mResultSet = other.mResultSet; | ||
75 | mIt = mResultSet.constBegin(); | ||
76 | mSkip = [this]() { | ||
77 | if (mIt != mResultSet.constEnd()) { | ||
78 | mIt++; | ||
79 | } | ||
80 | }; | ||
81 | } | ||
82 | } | ||
83 | |||
84 | bool ResultSet::next() | ||
85 | { | ||
86 | if (mIt) { | ||
87 | if (mIt != mResultSet.constEnd() && !mFirst) { | ||
88 | mIt++; | ||
89 | } | ||
90 | mFirst = false; | ||
91 | return mIt != mResultSet.constEnd(); | ||
92 | } else if (mGenerator) { | ||
93 | Q_ASSERT(mGenerator); | ||
94 | mCurrentValue = mGenerator(); | ||
95 | if (!mCurrentValue.isNull()) { | ||
96 | return true; | ||
97 | } | ||
98 | } else { | ||
99 | next([](const Sink::ApplicationDomain::ApplicationDomainType::Ptr &value, Sink::Operation){ return false; }); | ||
100 | } | ||
101 | return false; | ||
102 | } | ||
103 | |||
104 | bool ResultSet::next(std::function<bool(const Sink::ApplicationDomain::ApplicationDomainType::Ptr &value, Sink::Operation)> callback) | ||
105 | { | ||
106 | Q_ASSERT(mValueGenerator); | ||
107 | return mValueGenerator(callback); | ||
108 | } | ||
109 | |||
110 | void ResultSet::skip(int number) | ||
111 | { | ||
112 | Q_ASSERT(mSkip); | ||
113 | for (int i = 0; i < number; i++) { | ||
114 | mSkip(); | ||
115 | } | ||
116 | } | ||
117 | |||
118 | QByteArray ResultSet::id() | ||
119 | { | ||
120 | if (mIt) { | ||
121 | if (mIt == mResultSet.constEnd()) { | ||
122 | return QByteArray(); | ||
123 | } | ||
124 | Q_ASSERT(mIt != mResultSet.constEnd()); | ||
125 | return *mIt; | ||
126 | } else { | ||
127 | return mCurrentValue; | ||
128 | } | ||
129 | } | ||
130 | |||
131 | bool ResultSet::isEmpty() | ||
132 | { | ||
133 | return mResultSet.isEmpty(); | ||
134 | } | ||
diff --git a/common/resultset.h b/common/resultset.h index 8a0720d..e513460 100644 --- a/common/resultset.h +++ b/common/resultset.h | |||
@@ -34,100 +34,20 @@ class ResultSet { | |||
34 | typedef std::function<QByteArray()> IdGenerator; | 34 | typedef std::function<QByteArray()> IdGenerator; |
35 | typedef std::function<void()> SkipValue; | 35 | typedef std::function<void()> SkipValue; |
36 | 36 | ||
37 | ResultSet() | 37 | ResultSet(); |
38 | : mIt(nullptr) | 38 | ResultSet(const ValueGenerator &generator, const SkipValue &skip); |
39 | { | 39 | ResultSet(const IdGenerator &generator); |
40 | ResultSet(const QVector<QByteArray> &resultSet); | ||
41 | ResultSet(const ResultSet &other); | ||
40 | 42 | ||
41 | } | 43 | bool next(); |
44 | bool next(std::function<bool(const Sink::ApplicationDomain::ApplicationDomainType::Ptr &value, Sink::Operation)> callback); | ||
42 | 45 | ||
43 | ResultSet(const ValueGenerator &generator, const SkipValue &skip) | 46 | void skip(int number); |
44 | : mIt(nullptr), | ||
45 | mValueGenerator(generator), | ||
46 | mSkip(skip) | ||
47 | { | ||
48 | 47 | ||
49 | } | 48 | QByteArray id(); |
50 | 49 | ||
51 | ResultSet(const IdGenerator &generator) | 50 | bool isEmpty(); |
52 | : mIt(nullptr), | ||
53 | mGenerator(generator), | ||
54 | mSkip([this]() { | ||
55 | mGenerator(); | ||
56 | }) | ||
57 | { | ||
58 | |||
59 | } | ||
60 | |||
61 | ResultSet(const QVector<QByteArray> &resultSet) | ||
62 | : mResultSet(resultSet), | ||
63 | mIt(nullptr), | ||
64 | mSkip([this]() { | ||
65 | mGenerator(); | ||
66 | }) | ||
67 | { | ||
68 | |||
69 | } | ||
70 | |||
71 | bool next() | ||
72 | { | ||
73 | if (mGenerator) { | ||
74 | mCurrentValue = mGenerator(); | ||
75 | } else { | ||
76 | if (!mIt) { | ||
77 | mIt = mResultSet.constBegin(); | ||
78 | } else { | ||
79 | mIt++; | ||
80 | } | ||
81 | return mIt != mResultSet.constEnd(); | ||
82 | } | ||
83 | if (!mCurrentValue.isNull()) { | ||
84 | return true; | ||
85 | } | ||
86 | return false; | ||
87 | } | ||
88 | |||
89 | bool next(std::function<bool(const Sink::ApplicationDomain::ApplicationDomainType::Ptr &value, Sink::Operation)> callback) | ||
90 | { | ||
91 | Q_ASSERT(mValueGenerator); | ||
92 | return mValueGenerator(callback); | ||
93 | } | ||
94 | |||
95 | bool next(std::function<void(const QByteArray &key)> callback) | ||
96 | { | ||
97 | if (mGenerator) { | ||
98 | mCurrentValue = mGenerator(); | ||
99 | } else { | ||
100 | if (!mIt) { | ||
101 | mIt = mResultSet.constBegin(); | ||
102 | } else { | ||
103 | mIt++; | ||
104 | } | ||
105 | return mIt != mResultSet.constEnd(); | ||
106 | } | ||
107 | return false; | ||
108 | } | ||
109 | |||
110 | void skip(int number) | ||
111 | { | ||
112 | Q_ASSERT(mSkip); | ||
113 | for (int i = 0; i < number; i++) { | ||
114 | mSkip(); | ||
115 | } | ||
116 | } | ||
117 | |||
118 | QByteArray id() | ||
119 | { | ||
120 | if (mIt) { | ||
121 | return *mIt; | ||
122 | } else { | ||
123 | return mCurrentValue; | ||
124 | } | ||
125 | } | ||
126 | |||
127 | bool isEmpty() | ||
128 | { | ||
129 | return mResultSet.isEmpty(); | ||
130 | } | ||
131 | 51 | ||
132 | private: | 52 | private: |
133 | QVector<QByteArray> mResultSet; | 53 | QVector<QByteArray> mResultSet; |
@@ -136,5 +56,6 @@ class ResultSet { | |||
136 | IdGenerator mGenerator; | 56 | IdGenerator mGenerator; |
137 | ValueGenerator mValueGenerator; | 57 | ValueGenerator mValueGenerator; |
138 | SkipValue mSkip; | 58 | SkipValue mSkip; |
59 | bool mFirst; | ||
139 | }; | 60 | }; |
140 | 61 | ||
diff --git a/common/store.cpp b/common/store.cpp index 2f88c6d..6847d22 100644 --- a/common/store.cpp +++ b/common/store.cpp | |||
@@ -75,12 +75,13 @@ static QList<QByteArray> getResources(const QList<QByteArray> &resourceFilter, c | |||
75 | template <class DomainType> | 75 | template <class DomainType> |
76 | QSharedPointer<QAbstractItemModel> Store::loadModel(Query query) | 76 | QSharedPointer<QAbstractItemModel> Store::loadModel(Query query) |
77 | { | 77 | { |
78 | Trace() << "Query: "; | 78 | Trace() << "Query: " << ApplicationDomain::getTypeName<DomainType>(); |
79 | Trace() << " Requested: " << query.requestedProperties; | 79 | Trace() << " Requested: " << query.requestedProperties; |
80 | Trace() << " Filter: " << query.propertyFilter; | 80 | Trace() << " Filter: " << query.propertyFilter; |
81 | Trace() << " Parent: " << query.parentProperty; | 81 | Trace() << " Parent: " << query.parentProperty; |
82 | Trace() << " Ids: " << query.ids; | 82 | Trace() << " Ids: " << query.ids; |
83 | Trace() << " IsLive: " << query.liveQuery; | 83 | Trace() << " IsLive: " << query.liveQuery; |
84 | Trace() << " Sorting: " << query.sortProperty; | ||
84 | auto model = QSharedPointer<ModelResult<DomainType, typename DomainType::Ptr> >::create(query, query.requestedProperties); | 85 | auto model = QSharedPointer<ModelResult<DomainType, typename DomainType::Ptr> >::create(query, query.requestedProperties); |
85 | 86 | ||
86 | //* Client defines lifetime of model | 87 | //* Client defines lifetime of model |
diff --git a/common/typeindex.cpp b/common/typeindex.cpp index 3ee2492..ddf5df5 100644 --- a/common/typeindex.cpp +++ b/common/typeindex.cpp | |||
@@ -36,6 +36,10 @@ static QByteArray getByteArray(const QVariant &value) | |||
36 | 36 | ||
37 | static QByteArray toSortableByteArray(const QDateTime &date) | 37 | static QByteArray toSortableByteArray(const QDateTime &date) |
38 | { | 38 | { |
39 | //Sort invalid last | ||
40 | if (!date.isValid()) { | ||
41 | return QByteArray::number(std::numeric_limits<unsigned int>::max()); | ||
42 | } | ||
39 | return QByteArray::number(std::numeric_limits<unsigned int>::max() - date.toTime_t()); | 43 | return QByteArray::number(std::numeric_limits<unsigned int>::max() - date.toTime_t()); |
40 | } | 44 | } |
41 | 45 | ||
diff --git a/tests/querytest.cpp b/tests/querytest.cpp index 3bb0427..2531c25 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp | |||
@@ -305,16 +305,22 @@ private slots: | |||
305 | query.propertyFilter.insert("folder", folderEntity->identifier()); | 305 | query.propertyFilter.insert("folder", folderEntity->identifier()); |
306 | query.sortProperty = "date"; | 306 | query.sortProperty = "date"; |
307 | query.limit = 1; | 307 | query.limit = 1; |
308 | query.liveQuery = false; | ||
308 | 309 | ||
309 | //Ensure all local data is processed | 310 | //Ensure all local data is processed |
310 | Sink::ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished(); | 311 | Sink::ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished(); |
311 | 312 | ||
312 | //We fetch before the data is available and rely on the live query mechanism to deliver the actual data | ||
313 | auto model = Sink::Store::loadModel<Sink::ApplicationDomain::Mail>(query); | 313 | auto model = Sink::Store::loadModel<Sink::ApplicationDomain::Mail>(query); |
314 | QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); | 314 | QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); |
315 | //The model is not sorted, but the limited set is sorted, so we can only test for the latest result. | 315 | //The model is not sorted, but the limited set is sorted, so we can only test for the latest result. |
316 | QCOMPARE(model->rowCount(), 1); | 316 | QCOMPARE(model->rowCount(), 1); |
317 | QCOMPARE(model->index(0, 0).data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getProperty("uid").toByteArray(), QByteArray("testLatest")); | 317 | QCOMPARE(model->index(0, 0).data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getProperty("uid").toByteArray(), QByteArray("testLatest")); |
318 | |||
319 | model->fetchMore(QModelIndex()); | ||
320 | QTRY_VERIFY(model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()); | ||
321 | QCOMPARE(model->rowCount(), 2); | ||
322 | //We can't make any assumptions about the order of the indexes | ||
323 | // QCOMPARE(model->index(1, 0).data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getProperty("uid").toByteArray(), QByteArray("testSecond")); | ||
318 | } | 324 | } |
319 | }; | 325 | }; |
320 | 326 | ||