datastore.cpp 50.5 KB
Newer Older
Till Adam's avatar
Till Adam committed
1 2
/***************************************************************************
 *   Copyright (C) 2006 by Andreas Gungl <a.gungl@gmx.de>                  *
Volker Krause's avatar
Volker Krause committed
3
 *   Copyright (C) 2007 by Robert Zwerus <arzie@dds.nl>                    *
Till Adam's avatar
Till Adam committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Library General Public License as       *
 *   published by the Free Software Foundation; either version 2 of the    *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this program; if not, write to the                 *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
 ***************************************************************************/

21 22
#include "datastore.h"

23
#include "akonadi.h"
24
#include "dbconfig.h"
25
#include "dbinitializer.h"
26
#include "dbupdater.h"
27 28
#include "notificationmanager.h"
#include "tracer.h"
29
#include "transaction.h"
30 31 32
#include "selectquerybuilder.h"
#include "handlerhelper.h"
#include "countquerybuilder.h"
33
#include "parthelper.h"
34
#include "handler.h"
35
#include "collectionqueryhelper.h"
36
#include "akonadischema.h"
37
#include "parttypehelper.h"
38
#include "querycache.h"
39
#include "queryhelper.h"
40
#include "akonadiserver_debug.h"
41
#include "storagedebugger.h"
Daniel Vrátil's avatar
Daniel Vrátil committed
42
#include <utils.h>
43

44
#include <private/externalpartstorage_p.h>
45

Daniel Vrátil's avatar
Daniel Vrátil committed
46 47 48 49 50 51 52 53 54 55 56
#include <QCoreApplication>
#include <QString>
#include <QStringList>
#include <QThread>
#include <QThreadStorage>
#include <QTimer>
#include <QUuid>
#include <QVariant>
#include <QSqlDatabase>
#include <QSqlDriver>
#include <QSqlQuery>
Laurent Montel's avatar
Laurent Montel committed
57
#include <QFile>
58
#include <QElapsedTimer>
Till Adam's avatar
Till Adam committed
59

60
using namespace Akonadi;
61
using namespace Akonadi::Server;
Till Adam's avatar
Till Adam committed
62

63
static QMutex sTransactionMutex;
64 65
bool DataStore::s_hasForeignKeyConstraints = false;

66
QThreadStorage<DataStore *> DataStore::sInstances;
67

68 69
#define TRANSACTION_MUTEX_LOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.lock()
#define TRANSACTION_MUTEX_UNLOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.unlock()
70

71
#define setBoolPtr(ptr, val) \
Laurent Montel's avatar
Laurent Montel committed
72 73 74 75 76
    { \
        if ((ptr)) { \
            *(ptr) = (val); \
        } \
    }
77

Till Adam's avatar
Till Adam committed
78 79 80
/***************************************************************************
 *   DataStore                                                           *
 ***************************************************************************/
Guy Maurel's avatar
Guy Maurel committed
81
DataStore::DataStore()
82 83 84
    : QObject()
    , m_dbOpened(false)
    , m_transactionLevel(0)
Laurent Montel's avatar
Laurent Montel committed
85 86
    , mNotificationCollector(nullptr)
    , m_keepAliveTimer(nullptr)
Till Adam's avatar
Till Adam committed
87
{
88 89 90 91 92 93 94 95
    notificationCollector();

    if (DbConfig::configuredDatabase()->driverName() == QLatin1String("QMYSQL")) {
        // Send a dummy query to MySQL every 1 hour to keep the connection alive,
        // otherwise MySQL just drops the connection and our subsequent queries fail
        // without properly reporting the error
        m_keepAliveTimer = new QTimer(this);
        m_keepAliveTimer->setInterval(3600 * 1000);
96 97
        QObject::connect(m_keepAliveTimer, &QTimer::timeout,
                         this, &DataStore::sendKeepAliveQuery);
98 99
        m_keepAliveTimer->start();
    }
Till Adam's avatar
Till Adam committed
100 101
}

102 103
DataStore::~DataStore()
{
104 105 106
    if (m_dbOpened) {
        close();
    }
107 108 109
}

void DataStore::open()
Till Adam's avatar
Till Adam committed
110
{
111 112
    m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast<qulonglong>(QThread::currentThread()));
    Q_ASSERT(!QSqlDatabase::contains(m_connectionName));
113

114 115
    m_database = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), m_connectionName);
    DbConfig::configuredDatabase()->apply(m_database);
116

117 118 119 120 121
    if (!m_database.isValid()) {
        m_dbOpened = false;
        return;
    }
    m_dbOpened = m_database.open();
Till Adam's avatar
Till Adam committed
122

123 124 125
    if (!m_dbOpened) {
        debugLastDbError("Cannot open database.");
    } else {
126
        qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName();
127
    }
128

129 130 131 132 133 134 135 136 137 138
    StorageDebugger::instance()->addConnection(reinterpret_cast<qint64>(this),
                                               QThread::currentThread()->objectName());
    connect(QThread::currentThread(), &QThread::objectNameChanged,
            this, [this](const QString &name) {
                if (!name.isEmpty()) {
                    StorageDebugger::instance()->changeConnection(reinterpret_cast<qint64>(this),
                                                                  name);
                }
            });

139
    DbConfig::configuredDatabase()->initSession(m_database);
Till Adam's avatar
Till Adam committed
140 141
}

142 143 144 145 146 147 148 149
QSqlDatabase DataStore::database()
{
    if (!m_dbOpened) {
        open();
    }
    return m_database;
}

150
void DataStore::close()
Till Adam's avatar
Till Adam committed
151
{
152

153 154 155
    if (m_keepAliveTimer) {
        m_keepAliveTimer->stop();
    }
156

157 158 159
    if (!m_dbOpened) {
        return;
    }
160

161 162 163 164 165 166
    if (inTransaction()) {
        // By setting m_transactionLevel to '1' here, we skip all nested transactions
        // and rollback the outermost transaction.
        m_transactionLevel = 1;
        rollbackTransaction();
    }
167

168 169 170
    QueryCache::clear();
    m_database.close();
    m_database = QSqlDatabase();
171
    m_transactionQueries.clear();
172
    QSqlDatabase::removeDatabase(m_connectionName);
173

174 175
    StorageDebugger::instance()->removeConnection(reinterpret_cast<qint64>(this));

176
    m_dbOpened = false;
Till Adam's avatar
Till Adam committed
177 178
}

179
bool DataStore::init()
180
{
181
    Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread());
182

183
    AkonadiSchema schema;
184
    DbInitializer::Ptr initializer = DbInitializer::createInstance(database(), &schema);
185
    if (!initializer->run()) {
186
        qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
187 188 189
        return false;
    }
    s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints();
190

191
    if (QFile::exists(QStringLiteral(":dbupdate.xml"))) {
192
        DbUpdater updater(database(), QStringLiteral(":dbupdate.xml"));
193 194 195 196
        if (!updater.run()) {
            return false;
        }
    } else {
Laurent Montel's avatar
Laurent Montel committed
197
        qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates";
198
    }
199

200
    if (!initializer->updateIndexesAndConstraints()) {
201
        qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
202 203
        return false;
    }
204

205 206 207 208 209
    // enable caching for some tables
    MimeType::enableCache(true);
    Flag::enableCache(true);
    Resource::enableCache(true);
    Collection::enableCache(true);
Daniel Vrátil's avatar
Daniel Vrátil committed
210
    PartType::enableCache(true);
211

212
    return true;
213 214
}

215 216
NotificationCollector *DataStore::notificationCollector()
{
Laurent Montel's avatar
Laurent Montel committed
217
    if (mNotificationCollector == nullptr) {
218
        mNotificationCollector = new NotificationCollector(this);
219 220 221 222
        NotificationManager *notificationManager = AkonadiServer::instance()->notificationManager();
        if (notificationManager) {
            notificationManager->connectNotificationCollector(notificationCollector());
        }
223
    }
224

225
    return mNotificationCollector;
226
}
227

228
DataStore *DataStore::self()
229
{
230 231 232 233
    if (!sInstances.hasLocalData()) {
        sInstances.setLocalData(new DataStore());
    }
    return sInstances.localData();
234 235
}

236 237 238 239 240
bool DataStore::hasDataStore()
{
    return sInstances.hasLocalData();
}

Till Adam's avatar
Till Adam committed
241 242
/* --- ItemFlags ----------------------------------------------------- */

243
bool DataStore::setItemsFlags(const PimItem::List &items, const QVector<Flag> &flags,
244
                              bool *flagsChanged, const Collection &col_, bool silent)
Till Adam's avatar
Till Adam committed
245
{
246 247 248 249 250
    QSet<QByteArray> removedFlags;
    QSet<QByteArray> addedFlags;
    QVariantList insIds;
    QVariantList insFlags;
    Query::Condition delConds(Query::Or);
251
    Collection col = col_;
252 253 254

    setBoolPtr(flagsChanged, false);

Laurent Montel's avatar
Laurent Montel committed
255
    for (const PimItem &item : items) {
Daniel Vrátil's avatar
Daniel Vrátil committed
256
        const Flag::List itemFlags = item.flags();
257
        for (const Flag &flag : itemFlags) {
258 259 260 261 262 263 264 265
            if (!flags.contains(flag)) {
                removedFlags << flag.name().toLatin1();
                Query::Condition cond;
                cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id());
                cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id());
                delConds.addCondition(cond);
            }
        }
Till Adam's avatar
Till Adam committed
266

267
        Q_FOREACH (const Flag &flag, flags) {
Daniel Vrátil's avatar
Daniel Vrátil committed
268
            if (!itemFlags.contains(flag)) {
269 270 271 272 273
                addedFlags << flag.name().toLatin1();
                insIds << item.id();
                insFlags << flag.id();
            }
        }
274 275 276 277 278 279

        if (col.id() == -1) {
            col.setId(item.collectionId());
        } else if (col.id() != item.collectionId()) {
            col.setId(-2);
        }
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
    }

    if (!removedFlags.empty()) {
        QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
        qb.addCondition(delConds);
        if (!qb.exec()) {
            return false;
        }
    }

    if (!addedFlags.empty()) {
        QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
        qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds);
        qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags);
        qb2.setIdentificationColumn(QString());
        if (!qb2.exec()) {
            return false;
        }
    }

    if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) {
Daniel Vrátil's avatar
Daniel Vrátil committed
301
        mNotificationCollector->itemsFlagsChanged(items, addedFlags, removedFlags, col);
302
    }
303

304
    setBoolPtr(flagsChanged, (addedFlags != removedFlags));
305

306
    return true;
307 308
}

309
bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag,
310
                                  const QSet<Entity::Id> &existing, const Collection &col_,
311
                                  bool silent)
Till Adam's avatar
Till Adam committed
312
{
313
    Collection col = col_;
314 315 316 317 318 319 320 321 322 323 324
    QVariantList flagIds;
    QVariantList appendIds;
    PimItem::List appendItems;
    Q_FOREACH (const PimItem &item, items) {
        if (existing.contains(item.id())) {
            continue;
        }

        flagIds << flag.id();
        appendIds << item.id();
        appendItems << item;
325 326 327 328 329 330

        if (col.id() == -1) {
            col.setId(item.collectionId());
        } else if (col.id() != item.collectionId()) {
            col.setId(-2);
        }
331 332 333 334 335 336 337 338 339 340 341
    }

    if (appendItems.isEmpty()) {
        return true; // all items have the desired flags already
    }

    QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
    qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds);
    qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds);
    qb2.setIdentificationColumn(QString());
    if (!qb2.exec()) {
342
        qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb2.query().lastError();
343
        return false;
344
    }
345

346 347
    if (!silent) {
        mNotificationCollector->itemsFlagsChanged(appendItems, QSet<QByteArray>() << flag.name().toLatin1(),
Laurent Montel's avatar
Laurent Montel committed
348
                QSet<QByteArray>(), col);
349 350 351 352
    }

    return true;
}
353

354 355 356 357 358
bool DataStore::appendItemsFlags(const PimItem::List &items, const QVector<Flag> &flags,
                                 bool *flagsChanged, bool checkIfExists,
                                 const Collection &col, bool silent)
{
    QVariantList itemsIds;
359
    itemsIds.reserve(items.count());
360
    for (const PimItem &item : items) {
361
        itemsIds.append(item.id());
Till Adam's avatar
Till Adam committed
362
    }
363

364 365
    setBoolPtr(flagsChanged, false);

366
    for (const Flag &flag : flags) {
367 368 369 370 371 372 373 374 375 376
        QSet<PimItem::Id> existing;
        if (checkIfExists) {
            QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select);
            Query::Condition cond;
            cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id());
            cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds);
            qb.addColumn(PimItemFlagRelation::leftColumn());
            qb.addCondition(cond);

            if (!qb.exec()) {
377
                qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb.query().lastError();
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
                return false;
            }

            QSqlQuery query = qb.query();
            if (query.driver()->hasFeature(QSqlDriver::QuerySize)) {
                //The query size feature is not suppoerted by the sqllite driver
                if (query.size() == items.count()) {
                    continue;
                }
                setBoolPtr(flagsChanged, true);
            }

            while (query.next()) {
                existing << query.value(0).value<PimItem::Id>();
            }
            if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) {
                if (existing.size() != items.count()) {
                    setBoolPtr(flagsChanged, true);
                }
            }
        }

        if (!doAppendItemsFlag(items, flag, existing, col, silent)) {
            return false;
        }
403
    }
404

405
    return true;
Till Adam's avatar
Till Adam committed
406 407
}

408
bool DataStore::removeItemsFlags(const PimItem::List &items, const QVector<Flag> &flags,
409
                                 bool *flagsChanged, const Collection &col_, bool silent)
Till Adam's avatar
Till Adam committed
410
{
411
    Collection col = col_;
412 413 414 415 416
    QSet<QByteArray> removedFlags;
    QVariantList itemsIds;
    QVariantList flagsIds;

    setBoolPtr(flagsChanged, false);
417
    itemsIds.reserve(items.count());
418

419
    for (const PimItem &item : items) {
420
        itemsIds << item.id();
421 422 423 424 425
        if (col.id() == -1) {
            col.setId(item.collectionId());
        } else if (col.id() != item.collectionId()) {
            col.setId(-2);
        }
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
        for (int i = 0; i < flags.count(); ++i) {
            const QByteArray flagName = flags[i].name().toLatin1();
            if (!removedFlags.contains(flagName)) {
                flagsIds << flags[i].id();
                removedFlags << flagName;
            }
        }
    }

    // Delete all given flags from all given items in one go
    QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
    Query::Condition cond(Query::And);
    cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds);
    cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds);
    qb.addCondition(cond);
    if (!qb.exec()) {
        return false;
    }
444

445 446 447
    if (qb.query().numRowsAffected() != 0) {
        setBoolPtr(flagsChanged, true);
        if (!silent) {
Daniel Vrátil's avatar
Daniel Vrátil committed
448
            mNotificationCollector->itemsFlagsChanged(items, QSet<QByteArray>(), removedFlags, col);
449
        }
450
    }
451

452
    return true;
Till Adam's avatar
Till Adam committed
453 454
}

455 456
/* --- ItemTags ----------------------------------------------------- */

457 458
bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags,
                             bool *tagsChanged, bool silent)
459
{
460 461 462 463 464 465 466 467 468
    QSet<qint64> removedTags;
    QSet<qint64> addedTags;
    QVariantList insIds;
    QVariantList insTags;
    Query::Condition delConds(Query::Or);

    setBoolPtr(tagsChanged, false);

    Q_FOREACH (const PimItem &item, items) {
Daniel Vrátil's avatar
Daniel Vrátil committed
469 470
        const Tag::List itemTags = item.tags();
        Q_FOREACH (const Tag &tag, itemTags) {
471 472 473 474 475 476 477 478 479
            if (!tags.contains(tag)) {
                // Remove tags from items that had it set
                removedTags << tag.id();
                Query::Condition cond;
                cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id());
                cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id());
                delConds.addCondition(cond);
            }
        }
480

481
        Q_FOREACH (const Tag &tag, tags) {
Daniel Vrátil's avatar
Daniel Vrátil committed
482
            if (!itemTags.contains(tag)) {
483 484 485 486 487 488 489
                // Add tags to items that did not have the tag
                addedTags << tag.id();
                insIds << item.id();
                insTags << tag.id();
            }
        }
    }
490

491 492 493 494 495 496 497
    if (!removedTags.empty()) {
        QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
        qb.addCondition(delConds);
        if (!qb.exec()) {
            return false;
        }
    }
498

499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
    if (!addedTags.empty()) {
        QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
        qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds);
        qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags);
        qb2.setIdentificationColumn(QString());
        if (!qb2.exec()) {
            return false;
        }
    }

    if (!silent && (!addedTags.empty() || !removedTags.empty())) {
        mNotificationCollector->itemsTagsChanged(items, addedTags, removedTags);
    }

    setBoolPtr(tagsChanged, (addedTags != removedTags));

    return true;
516 517
}

518 519 520
bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag,
                                 const QSet<Entity::Id> &existing, const Collection &col,
                                 bool silent)
521
{
522 523 524
    QVariantList tagIds;
    QVariantList appendIds;
    PimItem::List appendItems;
525
    for (const PimItem &item : items) {
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
        if (existing.contains(item.id())) {
            continue;
        }

        tagIds << tag.id();
        appendIds << item.id();
        appendItems << item;
    }

    if (appendItems.isEmpty()) {
        return true; // all items have the desired tags already
    }

    QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
    qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds);
    qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds);
    qb2.setIdentificationColumn(QString());
    if (!qb2.exec()) {
544
        qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb2.query().lastError();
545
        return false;
546 547 548 549
    }

    if (!silent) {
        mNotificationCollector->itemsTagsChanged(appendItems, QSet<qint64>() << tag.id(),
Laurent Montel's avatar
Laurent Montel committed
550
                QSet<qint64>(), col);
551
    }
552

553 554 555 556 557 558 559 560
    return true;
}

bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags,
                                bool *tagsChanged, bool checkIfExists,
                                const Collection &col, bool silent)
{
    QVariantList itemsIds;
561
    itemsIds.reserve(items.count());
562
    for (const PimItem &item : items) {
563 564
        itemsIds.append(item.id());
    }
565

566 567
    setBoolPtr(tagsChanged, false);

568
    for (const Tag &tag : tags) {
569 570 571 572 573 574 575 576 577 578
        QSet<PimItem::Id> existing;
        if (checkIfExists) {
            QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select);
            Query::Condition cond;
            cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id());
            cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds);
            qb.addColumn(PimItemTagRelation::leftColumn());
            qb.addCondition(cond);

            if (!qb.exec()) {
579
                qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb.query().lastError();
580 581 582 583 584 585 586 587 588 589 590 591 592 593
                return false;
            }

            QSqlQuery query = qb.query();
            if (query.size() == items.count()) {
                continue;
            }

            setBoolPtr(tagsChanged, true);

            while (query.next()) {
                existing << query.value(0).value<PimItem::Id>();
            }
        }
594

595 596 597
        if (!doAppendItemsTag(items, tag, existing, col, silent)) {
            return false;
        }
598 599
    }

600
    return true;
601 602
}

603 604
bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags,
                                bool *tagsChanged, bool silent)
605
{
606 607 608 609 610
    QSet<qint64> removedTags;
    QVariantList itemsIds;
    QVariantList tagsIds;

    setBoolPtr(tagsChanged, false);
611
    itemsIds.reserve(items.count());
612 613 614 615 616 617 618 619 620 621 622

    Q_FOREACH (const PimItem &item, items) {
        itemsIds << item.id();
        for (int i = 0; i < tags.count(); ++i) {
            const qint64 tagId = tags[i].id();
            if (!removedTags.contains(tagId)) {
                tagsIds << tagId;
                removedTags << tagId;
            }
        }
    }
623

624 625 626 627 628 629 630 631
    // Delete all given tags from all given items in one go
    QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
    Query::Condition cond(Query::And);
    cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds);
    cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds);
    qb.addCondition(cond);
    if (!qb.exec()) {
        return false;
632 633
    }

634 635 636 637 638 639
    if (qb.query().numRowsAffected() != 0) {
        setBoolPtr(tagsChanged, true);
        if (!silent) {
            mNotificationCollector->itemsTagsChanged(items, QSet<qint64>(), removedTags);
        }
    }
640

Laurent Montel's avatar
Laurent Montel committed
641
    return true;
642 643 644 645
}

bool DataStore::removeTags(const Tag::List &tags, bool silent)
{
Daniel Vrátil's avatar
Daniel Vrátil committed
646 647 648
    // Currently the "silent" argument is only for API symmetry
    Q_UNUSED(silent);

649 650
    QVariantList removedTagsIds;
    QSet<qint64> removedTags;
651 652
    removedTagsIds.reserve(tags.count());
    removedTags.reserve(tags.count());
653 654 655 656 657 658 659 660 661 662 663
    Q_FOREACH (const Tag &tag, tags) {
        removedTagsIds << tag.id();
        removedTags << tag.id();
    }

    // Get all PIM items that we will untag
    SelectQueryBuilder<PimItem> itemsQuery;
    itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName());
    itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds);

    if (!itemsQuery.exec()) {
Laurent Montel's avatar
Laurent Montel committed
664
        qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError();
665 666 667 668 669 670 671 672 673 674 675 676 677
        return false;
    }
    const PimItem::List items = itemsQuery.result();

    if (!items.isEmpty()) {
        DataStore::self()->notificationCollector()->itemsTagsChanged(items, QSet<qint64>(), removedTags);
    }

    Q_FOREACH (const Tag &tag, tags) {
        // Emit special tagRemoved notification for each resource that owns the tag
        QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select);
        qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName());
        qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(),
Laurent Montel's avatar
Laurent Montel committed
678
                   TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName());
679 680 681
        qb.addColumn(Resource::nameFullColumnName());
        qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id());
        if (!qb.exec()) {
Laurent Montel's avatar
Laurent Montel committed
682
            qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << qb.query().lastError();
683 684 685 686 687 688
            return false;
        }

        // Emit specialized notifications for each resource
        QSqlQuery query = qb.query();
        while (query.next()) {
689 690
            const QString rid = query.value(0).toString();
            const QByteArray resource = query.value(1).toByteArray();
691 692 693 694 695 696 697 698 699 700 701 702

            DataStore::self()->notificationCollector()->tagRemoved(tag, resource, rid);
        }

        // And one for clients - without RID
        DataStore::self()->notificationCollector()->tagRemoved(tag, QByteArray(), QString());
    }

    // Just remove the tags, table constraints will take care of the rest
    QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete);
    qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds);
    if (!qb.exec()) {
Laurent Montel's avatar
Laurent Montel committed
703
        qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError();
704 705 706
        return false;
    }

707
    return true;
708 709
}

710 711
/* --- ItemParts ----------------------------------------------------- */

712
bool DataStore::removeItemParts(const PimItem &item, const QSet<QByteArray> &parts)
713
{
714 715 716 717 718 719 720
    SelectQueryBuilder<Part> qb;
    qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
    qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
    qb.addCondition(PartTypeHelper::conditionFromFqNames(parts));

    qb.exec();
    Part::List existingParts = qb.result();
Daniel Vrátil's avatar
Daniel Vrátil committed
721
    Q_FOREACH (Part part, existingParts) {  //krazy:exclude=foreach
722 723 724
        if (!PartHelper::remove(&part)) {
            return false;
        }
Volker Krause's avatar
Volker Krause committed
725
    }
726

727
    mNotificationCollector->itemChanged(item, parts);
728
    return true;
729
}
Till Adam's avatar
Till Adam committed
730

731
bool DataStore::invalidateItemCache(const PimItem &item)
732
{
733 734 735 736 737 738 739 740 741 742 743 744
    // find all payload item parts
    SelectQueryBuilder<Part> qb;
    qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName());
    qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
    qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
    qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
    qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD"));
    qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false);

    if (!qb.exec()) {
        return false;
    }
745

746 747
    const Part::List parts = qb.result();
    // clear data field
748
    for (Part part : parts) {
749 750 751
        if (!PartHelper::truncate(part)) {
            return false;
        }
752
    }
753

754
    return true;
755 756
}

757
/* --- Collection ------------------------------------------------------ */
758
bool DataStore::appendCollection(Collection &collection)
Till Adam's avatar
Till Adam committed
759
{
760 761 762 763 764
    // no need to check for already existing collection with the same name,
    // a unique index on parent + name prevents that in the database
    if (!collection.insert()) {
        return false;
    }
Till Adam's avatar
Till Adam committed
765

766 767
    mNotificationCollector->collectionAdded(collection);
    return true;
Till Adam's avatar
Till Adam committed
768 769
}

770
bool DataStore::cleanupCollection(Collection &collection)
771
{
772 773 774
    if (!s_hasForeignKeyConstraints) {
        return cleanupCollection_slow(collection);
    }
775

776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792
    // db will do most of the work for us, we just deal with notifications and external payload parts here
    Q_ASSERT(s_hasForeignKeyConstraints);

    // collect item deletion notifications
    const PimItem::List items = collection.items();
    const QByteArray resource = collection.resource().name().toLatin1();

    // generate the notification before actually removing the data
    // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though
    mNotificationCollector->itemsRemoved(items, collection, resource);

    // remove all external payload parts
    QueryBuilder qb(Part::tableName(), QueryBuilder::Select);
    qb.addColumn(Part::dataFullColumnName());
    qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName());
    qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName());
    qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id());
793
    qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External);
794 795 796 797 798 799 800
    qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
    if (!qb.exec()) {
        return false;
    }

    try {
        while (qb.query().next()) {
801 802
            ExternalPartStorage::self()->removePartFile(
                ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray()));
803 804
        }
    } catch (const PartHelperException &e) {
805
        qCDebug(AKONADISERVER_LOG) << e.what();
806
        return false;
Guy Maurel's avatar
Guy Maurel committed
807
    }
808

809 810 811
    // delete the collection itself, referential actions will do the rest
    mNotificationCollector->collectionRemoved(collection);
    return collection.remove();
812 813
}

814
bool DataStore::cleanupCollection_slow(Collection &collection)
815
{
816
    Q_ASSERT(!s_hasForeignKeyConstraints);
817

818 819 820 821
    // delete the content
    const PimItem::List items = collection.items();
    const QByteArray resource = collection.resource().name().toLatin1();
    mNotificationCollector->itemsRemoved(items, collection, resource);
822

823
    for (const PimItem &item : items) {
824 825 826 827 828 829
        if (!item.clearFlags()) {   // TODO: move out of loop and use only a single query
            return false;
        }
        if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) {     // TODO: reduce to single query
            return false;
        }
830

831 832 833
        if (!PimItem::remove(PimItem::idColumn(), item.id())) {     // TODO: move into single query
            return false;
        }
834

835 836 837
        if (!Entity::clearRelation<CollectionPimItemRelation>(item.id(), Entity::Right)) {     // TODO: move into single query
            return false;
        }
838
    }
839

840 841 842
    // delete collection mimetypes
    collection.clearMimeTypes();
    Collection::clearPimItems(collection.id());
843

844
    // delete attributes
Daniel Vrátil's avatar
Daniel Vrátil committed
845
    Q_FOREACH (CollectionAttribute attr, collection.attributes()) { //krazy:exclude=foreach
846 847 848
        if (!attr.remove()) {
            return false;
        }
849
    }
850

851 852 853
    // delete the collection itself
    mNotificationCollector->collectionRemoved(collection);
    return collection.remove();
854 855
}

856
static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId)
857
{
858
    Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID"));
859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881

    QueryBuilder qb(Collection::tableName(), QueryBuilder::Update);
    qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id());
    qb.setColumnValue(Collection::resourceIdColumn(), resourceId);
    qb.setColumnValue(Collection::remoteIdColumn(), QVariant());
    qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant());
    if (!qb.exec()) {
        return false;
    }

    // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc)
    // as well as mark the items dirty to prevent cache purging before they have been written back
    qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update);
    qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id());
    qb.setColumnValue(PimItem::remoteIdColumn(), QVariant());
    qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant());
    const QDateTime now = QDateTime::currentDateTime();
    qb.setColumnValue(PimItem::datetimeColumn(), now);
    qb.setColumnValue(PimItem::atimeColumn(), now);
    qb.setColumnValue(PimItem::dirtyColumn(), true);
    if (!qb.exec()) {
        return false;
    }
882

883
    transaction.commit();
884

885 886 887 888
    Q_FOREACH (const Collection &col, collection.children()) {
        if (!recursiveSetResourceId(col, resourceId)) {
            return false;
        }
889
    }
890
    return true;
891 892
}

893
bool DataStore::moveCollection(Collection &collection, const Collection &newParent)
Till Adam's avatar
Till Adam committed
894
{
895 896 897
    if (collection.parentId() == newParent.id()) {
        return true;
    }
898

899 900 901
    if (!m_dbOpened || !newParent.isValid()) {
        return false;
    }
902

903
    const QByteArray oldResource = collection.resource().name().toLatin1();
904

905 906 907 908 909 910 911 912
    int resourceId = collection.resourceId();
    const Collection source = collection.parent();
    if (newParent.id() > 0) {   // not root
        resourceId = newParent.resourceId();
    }
    if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) {
        return false;
    }
913

914 915 916 917 918 919 920 921
    collection.setParentId(newParent.id());
    if (collection.resourceId() != resourceId) {
        collection.setResourceId(resourceId);
        collection.setRemoteId(QString());
        collection.setRemoteRevision(QString());
        if (!recursiveSetResourceId(collection, resourceId)) {
            return false;
        }
922
    }
Till Adam's avatar
Till Adam committed
923

924 925 926
    if (!collection.update()) {
        return false;
    }
Till Adam's avatar
Till Adam committed
927

928 929
    mNotificationCollector->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1());
    return true;
930
}
Till Adam's avatar
Till Adam committed
931

932
bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes)
933
{
934 935 936
    if (mimeTypes.isEmpty()) {
        return true;
    }
Till Adam's avatar
Till Adam committed
937

938
    for (const QString &mimeType : mimeTypes) {
939 940
        const auto &mt = MimeType::retrieveByNameOrCreate(mimeType);
        if (!mt.isValid()) {
941 942
            return false;
        }
943
        if (!Collection::addMimeType(collectionId, mt.id())) {
944 945
            return false;
        }
946
    }
947

948
    return true;
Till Adam's avatar
Till Adam committed
949 950
}

951
void DataStore::activeCachePolicy(Collection &col)
Volker Krause's avatar
Volker Krause committed
952
{
953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972
    if (!col.cachePolicyInherit()) {
        return;
    }

    Collection parent = col;
    while (parent.parentId() != 0) {
        parent = parent.parent();
        if (!parent.cachePolicyInherit()) {
            col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval());
            col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout());
            col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand());
            col.setCachePolicyLocalParts(parent.cachePolicyLocalParts());
            return;
        }
    }

    // ### system default
    col.setCachePolicyCheckInterval(-1);
    col.setCachePolicyCacheTimeout(-1);
    col.setCachePolicySyncOnDemand(false);
973
    col.setCachePolicyLocalParts(QStringLiteral("ALL"));
Volker Krause's avatar
Volker Krause committed
974
}
975

976
QVector<Collection> DataStore::virtualCollections(const PimItem &item)
977
{
978 979 980 981 982 983
    SelectQueryBuilder<Collection> qb;
    qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(),
               Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName());
    qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id());

    if (!qb.exec()) {
984
        qCDebug(AKONADISERVER_LOG) << "Error during selection of records from table CollectionPimItemRelation"
Laurent Montel's avatar
Laurent Montel committed
985
                                   << qb.query().lastError().text();
986 987 988 989
        return QVector<Collection>();
    }

    return qb.result();
990 991
}

992
QMap<Entity::Id, QList<PimItem> > DataStore::virtualCollections(const PimItem::List &items)
993
{
994 995 996