Akonadi

collectionfetchhandler.cpp
1 /*
2  Copyright (c) 2007 Volker Krause <[email protected]>
3 
4  This library is free software; you can redistribute it and/or modify it
5  under the terms of the GNU Library General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or (at your
7  option) any later version.
8 
9  This library is distributed in the hope that it will be useful, but WITHOUT
10  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12  License for more details.
13 
14  You should have received a copy of the GNU Library General Public License
15  along with this library; see the file COPYING.LIB. If not, write to the
16  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  02110-1301, USA.
18 */
19 
20 #include "collectionfetchhandler.h"
21 #include "akonadiserver_debug.h"
22 
23 
24 #include "connection.h"
25 #include "handlerhelper.h"
26 #include "storage/datastore.h"
27 #include "storage/selectquerybuilder.h"
28 #include "storage/collectionqueryhelper.h"
29 
30 #include <private/scope_p.h>
31 
32 using namespace Akonadi;
33 using namespace Akonadi::Server;
34 
35 template <typename T>
36 static bool intersect(const QVector<typename T::Id> &l1, const QVector<T> &l2)
37 {
38  for (const T &e2 : l2) {
39  if (l1.contains(e2.id())) {
40  return true;
41  }
42  }
43  return false;
44 }
45 
46 CollectionFetchHandler::CollectionFetchHandler(AkonadiServer &akonadi)
47  : Handler(akonadi)
48 {}
49 
50 QStack<Collection> CollectionFetchHandler::ancestorsForCollection(const Collection &col)
51 {
52  if (mAncestorDepth <= 0) {
53  return QStack<Collection>();
54  }
55  QStack<Collection> ancestors;
56  Collection parent = col;
57  for (int i = 0; i < mAncestorDepth; ++i) {
58  if (parent.parentId() == 0) {
59  break;
60  }
61  if (mAncestors.contains(parent.parentId())) {
62  parent = mAncestors.value(parent.parentId());
63  } else {
64  parent = mCollections.value(parent.parentId());
65  }
66  if (!parent.isValid()) {
67  qCWarning(AKONADISERVER_LOG) << "Found an invalid parent in ancestors of Collection" << col.name()
68  << "(ID:" << col.id() << ")";
69  throw HandlerException("Found invalid parent in ancestors");
70  }
71  ancestors.prepend(parent);
72  }
73  return ancestors;
74 }
75 
76 CollectionAttribute::List CollectionFetchHandler::getAttributes(const Collection &col,
77  const QSet<QByteArray> &filter)
78 {
79  CollectionAttribute::List attributes;
80  auto it = mCollectionAttributes.find(col.id());
81  while (it != mCollectionAttributes.end() && it.key() == col.id()) {
82  if (filter.isEmpty() || filter.contains(it.value().type())) {
83  attributes << it.value();
84  }
85  ++it;
86  }
87 
88  {
90  attr.setType(AKONADI_PARAM_ENABLED);
91  attr.setValue(col.enabled() ? "TRUE" : "FALSE");
92  attributes << attr;
93  }
94 
95  return attributes;
96 }
97 
98 void CollectionFetchHandler::listCollection(const Collection &root,
99  const QStack<Collection> &ancestors,
100  const QStringList &mimeTypes,
101  const CollectionAttribute::List &attributes)
102 {
103  QStack<CollectionAttribute::List> ancestorAttributes;
104  //backwards compatibility, collectionToByteArray will automatically fall-back to id + remoteid
105  if (!mAncestorAttributes.isEmpty()) {
106  ancestorAttributes.reserve(ancestors.size());
107  for (const Collection &col : ancestors) {
108  ancestorAttributes.push(getAttributes(col, mAncestorAttributes));
109  }
110  }
111 
112  // write out collection details
113  Collection dummy = root;
114  storageBackend()->activeCachePolicy(dummy);
115 
116  sendResponse(HandlerHelper::fetchCollectionsResponse(akonadi(), dummy, attributes, mIncludeStatistics,
117  mAncestorDepth, ancestors,
118  ancestorAttributes,
119  mimeTypes));
120 }
121 
122 static Query::Condition filterCondition(const QString &column)
123 {
124  Query::Condition orCondition(Query::Or);
125  orCondition.addValueCondition(column, Query::Equals, (int)Collection::True);
126  Query::Condition andCondition(Query::And);
127  andCondition.addValueCondition(column, Query::Equals, (int)Collection::Undefined);
128  andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
129  orCondition.addCondition(andCondition);
130  return orCondition;
131 }
132 
133 bool CollectionFetchHandler::checkFilterCondition(const Collection &col) const
134 {
135  //Don't include the collection when only looking for enabled collections
136  if (mEnabledCollections && !col.enabled()) {
137  return false;
138  }
139  //Don't include the collection when only looking for collections to display/index/sync
140  if (mCollectionsToDisplay &&
141  (((col.displayPref() == Collection::Undefined) && !col.enabled()) ||
142  (col.displayPref() == Collection::False))) {
143  return false;
144  }
145  if (mCollectionsToIndex &&
146  (((col.indexPref() == Collection::Undefined) && !col.enabled()) ||
147  (col.indexPref() == Collection::False))) {
148  return false;
149  }
150  //Single collection sync will still work since that is using a base fetch
151  if (mCollectionsToSynchronize &&
152  (((col.syncPref() == Collection::Undefined) && !col.enabled()) ||
153  (col.syncPref() == Collection::False))) {
154  return false;
155  }
156  return true;
157 }
158 
159 static QSqlQuery getAttributeQuery(const QVariantList &ids, const QSet<QByteArray> &requestedAttributes)
160 {
161  QueryBuilder qb(CollectionAttribute::tableName());
162 
163  qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::In, ids);
164 
165  qb.addColumn(CollectionAttribute::collectionIdFullColumnName());
166  qb.addColumn(CollectionAttribute::typeFullColumnName());
167  qb.addColumn(CollectionAttribute::valueFullColumnName());
168 
169  if (!requestedAttributes.isEmpty()) {
170  QVariantList attributes;
171  attributes.reserve(requestedAttributes.size());
172  for (const QByteArray &type : requestedAttributes) {
173  attributes << type;
174  }
175  qb.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::In, attributes);
176  }
177 
178  qb.addSortColumn(CollectionAttribute::collectionIdFullColumnName(), Query::Ascending);
179 
180  if (!qb.exec()) {
181  throw HandlerException("Unable to retrieve attributes for listing");
182  }
183  return qb.query();
184 }
185 
186 void CollectionFetchHandler::retrieveAttributes(const QVariantList &collectionIds)
187 {
188  //We are querying for the attributes in batches because something can't handle WHERE IN queries with sets larger than 999
189  int start = 0;
190  const int size = 999;
191  while (start < collectionIds.size()) {
192  const QVariantList ids = collectionIds.mid(start, size);
193  QSqlQuery attributeQuery = getAttributeQuery(ids, mAncestorAttributes);
194  while (attributeQuery.next()) {
195  CollectionAttribute attr;
196  attr.setType(attributeQuery.value(1).toByteArray());
197  attr.setValue(attributeQuery.value(2).toByteArray());
198  // qCDebug(AKONADISERVER_LOG) << "found attribute " << attr.type() << attr.value();
199  mCollectionAttributes.insert(attributeQuery.value(0).toLongLong(), attr);
200  }
201  attributeQuery.finish();
202  start += size;
203  }
204 }
205 
206 static QSqlQuery getMimeTypeQuery(const QVariantList &ids)
207 {
208  QueryBuilder qb(CollectionMimeTypeRelation::tableName());
209 
210  qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), MimeType::idFullColumnName(), CollectionMimeTypeRelation::rightFullColumnName());
211  qb.addValueCondition(CollectionMimeTypeRelation::leftFullColumnName(), Query::In, ids);
212 
213  qb.addColumn(CollectionMimeTypeRelation::leftFullColumnName());
214  qb.addColumn(CollectionMimeTypeRelation::rightFullColumnName());
215  qb.addColumn(MimeType::nameFullColumnName());
216  qb.addSortColumn(CollectionMimeTypeRelation::leftFullColumnName(), Query::Ascending);
217 
218  if (!qb.exec()) {
219  throw HandlerException("Unable to retrieve mimetypes for listing");
220  }
221  return qb.query();
222 }
223 
224 void CollectionFetchHandler::retrieveCollections(const Collection &topParent, int depth)
225 {
226  /*
227  * Retrieval of collections:
228  * The aim is to reduce the amount of queries as much as possible, as this has the largest performance impact for large queries.
229  * * First all collections that match the given criteria are queried
230  * * We then filter the false positives:
231  * ** all collections out that are not part of the tree we asked for are filtered
232  * * Finally we complete the tree by adding missing collections
233  *
234  * Mimetypes and attributes are also retrieved in single queries to avoid spawning two queries per collection (the N+1 problem).
235  * Note that we're not querying attributes and mimetypes for the collections that are only included to complete the tree,
236  * this results in no items being queried for those collections.
237  */
238 
239  const qint64 parentId = topParent.isValid() ? topParent.id() : 0;
240  {
242 
243  if (depth == 0) {
244  qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, parentId);
245  } else if (depth == 1) {
246  if (topParent.isValid()) {
247  qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Equals, parentId);
248  } else {
249  qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant());
250  }
251  } else {
252  if (topParent.isValid()) {
253  qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, topParent.resourceId());
254  } else {
255  // Gimme gimme gimme...everything!
256  }
257  }
258 
259  //Base listings should succeed always
260  if (depth != 0) {
261  if (mCollectionsToSynchronize) {
262  qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
263  } else if (mCollectionsToDisplay) {
264  qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
265  } else if (mCollectionsToIndex) {
266  qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
267  } else if (mEnabledCollections) {
268  qb.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
269  }
270  if (mResource.isValid()) {
271  qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, mResource.id());
272  }
273 
274  if (!mMimeTypes.isEmpty()) {
275  qb.addJoin(QueryBuilder::LeftJoin, CollectionMimeTypeRelation::tableName(), CollectionMimeTypeRelation::leftColumn(), Collection::idFullColumnName());
276  QVariantList mimeTypeFilter;
277  mimeTypeFilter.reserve(mMimeTypes.size());
278  for (MimeType::Id mtId : qAsConst(mMimeTypes)) {
279  mimeTypeFilter << mtId;
280  }
281  qb.addValueCondition(CollectionMimeTypeRelation::rightColumn(), Query::In, mimeTypeFilter);
282  qb.addGroupColumn(Collection::idFullColumnName());
283  }
284  }
285 
286  if (!qb.exec()) {
287  throw HandlerException("Unable to retrieve collection for listing");
288  }
289  Q_FOREACH (const Collection &col, qb.result()) {
290  mCollections.insert(col.id(), col);
291  }
292  }
293 
294  //Post filtering that we couldn't do as part of the sql query
295  if (depth > 0) {
296  auto it = mCollections.begin();
297  while (it != mCollections.end()) {
298 
299  if (topParent.isValid()) {
300  //Check that each collection is linked to the root collection
301  bool foundParent = false;
302  //We iterate over parents to link it to topParent if possible
303  Collection::Id id = it->parentId();
304  while (id > 0) {
305  if (id == parentId) {
306  foundParent = true;
307  break;
308  }
309  Collection col = mCollections.value(id);
310  if (!col.isValid()) {
311  col = Collection::retrieveById(id);
312  }
313  id = col.parentId();
314  }
315  if (!foundParent) {
316  it = mCollections.erase(it);
317  continue;
318  }
319  }
320  ++it;
321  }
322  }
323 
324  QVariantList mimeTypeIds;
325  QVariantList attributeIds;
326  QVariantList ancestorIds;
327  const int collectionSize{mCollections.size()};
328  mimeTypeIds.reserve(collectionSize);
329  attributeIds.reserve(collectionSize);
330  //We'd only require the non-leaf collections, but we don't know which those are, so we take all.
331  ancestorIds.reserve(collectionSize);
332  for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) {
333  mimeTypeIds << it.key();
334  attributeIds << it.key();
335  ancestorIds << it.key();
336  }
337 
338  if (mAncestorDepth > 0 && topParent.isValid()) {
339  //unless depth is 0 the base collection is not part of the listing
340  mAncestors.insert(topParent.id(), topParent);
341  ancestorIds << topParent.id();
342  //We need to retrieve additional ancestors to what we already have in the tree
343  Collection parent = topParent;
344  for (int i = 0; i < mAncestorDepth; ++i) {
345  if (parent.parentId() == 0) {
346  break;
347  }
348  parent = parent.parent();
349  mAncestors.insert(parent.id(), parent);
350  //We also require the attributes
351  ancestorIds << parent.id();
352  }
353  }
354 
355  QSet<qint64> missingCollections;
356  if (depth > 0) {
357  for (const Collection &col : qAsConst(mCollections)) {
358  if (col.parentId() != parentId && !mCollections.contains(col.parentId())) {
359  missingCollections.insert(col.parentId());
360  }
361  }
362  }
363 
364  /*
365  QSet<qint64> knownIds;
366  for (const Collection &col : mCollections) {
367  knownIds.insert(col.id());
368  }
369  qCDebug(AKONADISERVER_LOG) << "HAS:" << knownIds;
370  qCDebug(AKONADISERVER_LOG) << "MISSING:" << missingCollections;
371  */
372 
373  //Fetch missing collections that are part of the tree
374  while (!missingCollections.isEmpty()) {
376  QVariantList ids;
377  ids.reserve(missingCollections.size());
378  for (qint64 id : qAsConst(missingCollections)) {
379  ids << id;
380  }
381  qb.addValueCondition(Collection::idFullColumnName(), Query::In, ids);
382  if (!qb.exec()) {
383  throw HandlerException("Unable to retrieve collections for listing");
384  }
385 
386  missingCollections.clear();
387  Q_FOREACH (const Collection &missingCol, qb.result()) {
388  mCollections.insert(missingCol.id(), missingCol);
389  ancestorIds << missingCol.id();
390  attributeIds << missingCol.id();
391  mimeTypeIds << missingCol.id();
392  //We have to do another round if the parents parent is missing
393  if (missingCol.parentId() != parentId && !mCollections.contains(missingCol.parentId())) {
394  missingCollections.insert(missingCol.parentId());
395  }
396  }
397  }
398 
399  //Since we don't know when we'll need the ancestor attributes, we have to fetch them all together.
400  //The alternative would be to query for each collection which would reintroduce the N+1 query performance problem.
401  if (!mAncestorAttributes.isEmpty()) {
402  retrieveAttributes(ancestorIds);
403  }
404 
405  //We are querying in batches because something can't handle WHERE IN queries with sets larger than 999
406  const int querySizeLimit = 999;
407  int mimetypeQueryStart = 0;
408  int attributeQueryStart = 0;
409  QSqlQuery mimeTypeQuery(storageBackend()->database());
410  QSqlQuery attributeQuery(storageBackend()->database());
411  auto it = mCollections.begin();
412  while (it != mCollections.end()) {
413  const Collection col = it.value();
414 
415  QStringList mimeTypes;
416  {
417  //Get new query if necessary
418  if (!mimeTypeQuery.isValid() && mimetypeQueryStart < mimeTypeIds.size()) {
419  const QVariantList ids = mimeTypeIds.mid(mimetypeQueryStart, querySizeLimit);
420  mimetypeQueryStart += querySizeLimit;
421  mimeTypeQuery = getMimeTypeQuery(ids);
422  mimeTypeQuery.next(); //place at first record
423  }
424 
425  while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() < col.id()) {
426  if (!mimeTypeQuery.next()) {
427  break;
428  }
429  }
430  //Advance query while a mimetype for this collection is returned
431  while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() == col.id()) {
432  mimeTypes << mimeTypeQuery.value(2).toString();
433  if (!mimeTypeQuery.next()) {
434  break;
435  }
436  }
437  }
438 
439  CollectionAttribute::List attributes;
440  {
441  //Get new query if necessary
442  if (!attributeQuery.isValid() && attributeQueryStart < attributeIds.size()) {
443  const QVariantList ids = attributeIds.mid(attributeQueryStart, querySizeLimit);
444  attributeQueryStart += querySizeLimit;
445  attributeQuery = getAttributeQuery(ids, QSet<QByteArray>());
446  attributeQuery.next(); //place at first record
447  }
448 
449  while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() < col.id()) {
450  if (!attributeQuery.next()) {
451  break;
452  }
453  }
454  //Advance query while a mimetype for this collection is returned
455  while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() == col.id()) {
456  CollectionAttribute attr;
457  attr.setType(attributeQuery.value(1).toByteArray());
458  attr.setValue(attributeQuery.value(2).toByteArray());
459  attributes << attr;
460 
461  if (!attributeQuery.next()) {
462  break;
463  }
464  }
465  }
466 
467  listCollection(col, ancestorsForCollection(col), mimeTypes, attributes);
468  it++;
469  }
470  attributeQuery.finish();
471  mimeTypeQuery.finish();
472 }
473 
474 bool CollectionFetchHandler::parseStream()
475 {
476  const auto &cmd = Protocol::cmdCast<Protocol::FetchCollectionsCommand>(m_command);
477 
478  if (!cmd.resource().isEmpty()) {
479  mResource = Resource::retrieveByName(cmd.resource());
480  if (!mResource.isValid()) {
481  return failureResponse("Unknown resource");
482  }
483  }
484  const QStringList lstMimeTypes = cmd.mimeTypes();
485  for (const QString &mtName : lstMimeTypes) {
486  const MimeType mt = MimeType::retrieveByNameOrCreate(mtName);
487  if (!mt.isValid()) {
488  return failureResponse("Failed to create mimetype record");
489  }
490  mMimeTypes.append(mt.id());
491  }
492 
493  mEnabledCollections = cmd.enabled();
494  mCollectionsToSynchronize = cmd.syncPref();
495  mCollectionsToDisplay = cmd.displayPref();
496  mCollectionsToIndex = cmd.indexPref();
497  mIncludeStatistics = cmd.fetchStats();
498 
499  int depth = 0;
500  switch (cmd.depth()) {
501  case Protocol::FetchCollectionsCommand::BaseCollection:
502  depth = 0;
503  break;
504  case Protocol::FetchCollectionsCommand::ParentCollection:
505  depth = 1;
506  break;
507  case Protocol::FetchCollectionsCommand::AllCollections:
508  depth = INT_MAX;
509  break;
510  }
511 
512  switch (cmd.ancestorsDepth()) {
513  case Protocol::Ancestor::NoAncestor:
514  mAncestorDepth = 0;
515  break;
516  case Protocol::Ancestor::ParentAncestor:
517  mAncestorDepth = 1;
518  break;
519  case Protocol::Ancestor::AllAncestors:
520  mAncestorDepth = INT_MAX;
521  break;
522  }
523  mAncestorAttributes = cmd.ancestorsAttributes();
524 
525  Scope scope = cmd.collections();
526  if (!scope.isEmpty()) { // not root
527  Collection col;
528  if (scope.scope() == Scope::Uid) {
529  col = Collection::retrieveById(scope.uid());
530  } else if (scope.scope() == Scope::Rid) {
532  qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, scope.rid());
533  qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(),
534  Collection::resourceIdFullColumnName(), Resource::idFullColumnName());
535  if (mCollectionsToSynchronize) {
536  qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
537  } else if (mCollectionsToDisplay) {
538  qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
539  } else if (mCollectionsToIndex) {
540  qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
541  }
542  if (mResource.isValid()) {
543  qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, mResource.id());
544  } else if (connection()->context().resource().isValid()) {
545  qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, connection()->context().resource().id());
546  } else {
547  return failureResponse("Cannot retrieve collection based on remote identifier without a resource context");
548  }
549  if (!qb.exec()) {
550  return failureResponse("Unable to retrieve collection for listing");
551  }
552  Collection::List results = qb.result();
553  if (results.count() != 1) {
554  return failureResponse(QString::number(results.count()) + QStringLiteral(" collections found"));
555  }
556  col = results.first();
557  } else if (scope.scope() == Scope::HierarchicalRid) {
558  if (!connection()->context().resource().isValid()) {
559  return failureResponse("Cannot retrieve collection based on hierarchical remote identifier without a resource context");
560  }
561  col = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection()->context().resource().id());
562  } else {
563  return failureResponse("Unexpected error");
564  }
565 
566  if (!col.isValid()) {
567  return failureResponse("Collection does not exist");
568  }
569 
570  retrieveCollections(col, depth);
571  } else { //Root folder listing
572  if (depth != 0) {
573  retrieveCollections(Collection(), depth);
574  }
575  }
576 
577  return successResponse<Protocol::FetchCollectionsResponse>();
578 }
qlonglong toLongLong(bool *ok) const const
void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type=WhereCondition)
Add a WHERE or HAVING condition which compares a column with a given value.
QByteArray toByteArray() const const
Type type(const QString &mimeType)
void setType(const QByteArray &type)
Sets the value of the type column of this record.
Definition: entities.cpp:7218
bool isValid() const
Returns whether the collection is valid.
Definition: collection.cpp:137
QString name() const
Returns the i18n&#39;ed name of the collection.
Definition: collection.cpp:225
int size() const const
void reserve(int alloc)
Representation of a record in the MimeType table.
Definition: entities.h:1011
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition: handler.h:48
void push(const T &t)
Represents a collection of PIM items.
Definition: collection.h:76
qint64 Id
Describes the unique id type.
Definition: collection.h:82
void addJoin(JoinType joinType, const QString &table, const Query::Condition &condition)
Join a table to the query.
T & first()
QSet::iterator insert(const T &value)
void setValue(const QByteArray &value)
Sets the value of the value column of this record.
Definition: entities.cpp:7231
T value(int i) const const
T value(int i) const const
QSize size() const const
QString number(int n, int base)
bool contains(const T &value) const const
QVariant value(int index) const const
void addGroupColumn(const QString &column)
Add a GROUP BY column.
bool next()
QVector< T > result()
Returns the result of this SELECT query.
QVector< T > mid(int pos, int length) const const
void reserve(int size)
QStringList mimeTypeFilter() const
Returns the mime types any of which the selected collection(s) shall support.
Representation of a record in the CollectionAttribute table.
Definition: entities.h:2254
bool contains(const T &value) const const
const QList< QKeySequence > & end()
bool isValid() const const
Helper class for creating and executing database SELECT queries.
Id id() const
Returns the unique identifier of the collection.
Definition: collection.cpp:112
Definition: item.h:44
void finish()
Helper integration between Akonadi and Qt.
void prepend(T &&value)
int count(const T &value) const const
QList< T > mid(int pos, int length) const const
bool isEmpty() const const
Represents a WHERE condition tree.
Definition: query.h:77
bool enabled() const
Returns the collection&#39;s enabled state.
Definition: collection.cpp:380
void clear()
QObject * parent() const const
int size() const const
int depth() const const
Helper class to construct arbitrary SQL queries.
Definition: querybuilder.h:45
void addCondition(const Query::Condition &condition, ConditionType type=WhereCondition)
Add a WHERE condition.
bool exec()
Executes the query, returns true on success.
Representation of a record in the Collection table.
Definition: entities.h:451
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Wed May 27 2020 22:43:37 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.