Krita Source Code Documentation
Loading...
Searching...
No Matches
KisResourceCacheDb.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
7
8#include <QSqlError>
9#include <QSqlQuery>
10#include <QSqlDatabase>
11
12#include <QBuffer>
13#include <QVersionNumber>
14#include <QStandardPaths>
15#include <QDir>
16#include <QDirIterator>
17#include <QStringList>
18#include <QElapsedTimer>
19#include <QDataStream>
20#include <QByteArray>
21#include <QMessageBox>
22
23#include <KritaVersionWrapper.h>
24
25#include <klocalizedstring.h>
26#include <KisBackup.h>
27
28#include <kis_debug.h>
29#include <KisUsageLogger.h>
30
31#include <KisSqlQueryLoader.h>
33#include "KisResourceLocator.h"
35
36#include "ResourceDebug.h"
37#include <kis_assert.h>
38
39#include <KisCppQuirks.h>
40
41const QString dbDriver = "QSQLITE";
42const QString METADATA_RESOURCES = "resources";
43const QString METADATA_STORAGES = "storages";
44
45const QString KisResourceCacheDb::resourceCacheDbFilename { "resourcecache.sqlite" };
46const QString KisResourceCacheDb::databaseVersion { "0.0.18" };
48QStringList KisResourceCacheDb::disabledBundles { QStringList() << "Krita_3_Default_Resources.bundle" };
49
51QString KisResourceCacheDb::s_lastError {QString()};
52
54{
55 return s_valid;
56}
57
59{
60 return s_lastError;
61}
62
63// use in WHERE QSqlQuery clauses
64// because if the string is null, the query will also have null there
65// and every comparison with null is false, so the query won't find anything
66// (especially important for storage location where empty string is common)
67QString changeToEmptyIfNull(QString s)
68{
69 return s.isNull() ? QString("") : s;
70}
71
73{
74 QFile f(":/fill_version_information.sql");
75 if (f.open(QFile::ReadOnly)) {
76 QString sql = f.readAll();
77 QSqlQuery q;
78 if (!q.prepare(sql)) {
79 warnDbMigration << "Could not prepare the schema information query" << q.lastError() << q.boundValues();
80 return false;
81 }
83 q.addBindValue(KritaVersionWrapper::versionString());
84 q.addBindValue(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
85 if (!q.exec()) {
86 warnDbMigration << "Could not insert the current version" << q.lastError() << q.boundValues();
87 return false;
88 }
89
90 infoDbMigration << "Filled version table";
91 }
92 return true;
93}
94
95QSqlError runUpdateScriptFile(const QString &path, const QString &message)
96{
97 try {
98
99 KisSqlQueryLoader loader(path);
100 loader.exec();
101
102 } catch (const KisSqlQueryLoader::FileException &e) {
103 warnDbMigration.noquote() << "ERROR: Could not execute DB update step:" << message;
104 warnDbMigration.noquote() << " error" << e.message;
105 warnDbMigration.noquote() << " file:" << e.filePath;
106 warnDbMigration.noquote() << " file-error:" << e.fileErrorString;
107 return
108 QSqlError("Error executing SQL",
109 QString("Could not find SQL file %1").arg(e.filePath),
110 QSqlError::StatementError);
111 } catch (const KisSqlQueryLoader::SQLException &e) {
112 warnDbMigration.noquote() << "ERROR: Could not execute DB update step:" << message;
113 warnDbMigration.noquote() << " error" << e.message;
114 warnDbMigration.noquote() << " file:" << e.filePath;
115 warnDbMigration.noquote() << " statement:" << e.statementIndex;
116 warnDbMigration.noquote() << " sql-error:" << e.sqlError.text();
117 return e.sqlError;
118 }
119
120 infoDbMigration << "Completed DB update step:" << message;
121 return QSqlError();
122}
123
124QSqlError runUpdateScript(const QString &script, const QString &message)
125{
126 try {
127
128 KisSqlQueryLoader loader("", script);
129 loader.exec();
130
131 } catch (const KisSqlQueryLoader::SQLException &e) {
132 warnDbMigration.noquote() << "ERROR: Could execute DB update step:" << message;
133 warnDbMigration.noquote() << " error" << e.message;
134 warnDbMigration.noquote() << " sql-error:" << e.sqlError.text();
135 return e.sqlError;
136 }
137
138 infoDbMigration << "Completed DB update step:" << message;
139 return QSqlError();
140}
141
142QSqlError createDatabase(const QString &location)
143{
144 // NOTE: if the id's of Unknown and Memory in the database
145 // will change, and that will break the queries that
146 // remove Unknown and Memory storages on start-up.
155
156 QDir dbLocation(location);
157 if (!dbLocation.exists()) {
158 dbLocation.mkpath(dbLocation.path());
159 }
160
161 std::optional<QSqlDatabase> existingDatabase =
162 QSqlDatabase::database(QSqlDatabase::defaultConnection, false);
163
164 const bool databaseConnectionExists = !QSqlDatabase::connectionNames().isEmpty()
165 && existingDatabase->isValid() && existingDatabase->isOpen();
166
167 if (databaseConnectionExists && existingDatabase->tables().contains("version_information")) {
168 return QSqlError();
169 }
170
171 existingDatabase = std::nullopt;
172
173 QSqlDatabase db;
174
175 if (!databaseConnectionExists) {
176 db = QSqlDatabase::addDatabase(dbDriver);
177 db.setDatabaseName(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
178
179 if (!db.open()) {
180 warnDbMigration << "Could not connect to resource cache database";
181 return db.lastError();
182 }
183 } else {
184 db = QSqlDatabase::database();
185 }
186
187 // will be filled correctly later
188 QVersionNumber oldSchemaVersionNumber;
189 QVersionNumber newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
190
191
192 QStringList tables = QStringList() << "version_information"
193 << "storage_types"
194 << "resource_types"
195 << "storages"
196 << "tags"
197 << "resources"
198 << "versioned_resources"
199 << "resource_tags"
200 << "metadata"
201 << "tags_storages"
202 << "tag_translations";
203
204 QStringList dbTables;
205 // Verify whether we should recreate the database
206 {
207 bool allTablesPresent = true;
208 dbTables = db.tables();
209 Q_FOREACH(const QString &table, tables) {
210 if (!dbTables.contains(table)) {
211 allTablesPresent = false;
212 break;
213 }
214 }
215
216 bool schemaIsOutDated = false;
217 QString schemaVersion = "0.0.0";
218 QString kritaVersion = "Unknown";
219 int creationDate = 0;
220
221 if (dbTables.contains("version_information")) {
222 // Verify the version number
223
224 {
225 QSqlQuery q(
226 "SELECT database_version\n"
227 ", krita_version\n"
228 ", creation_date\n"
229 "FROM version_information\n"
230 "ORDER BY id\n"
231 "DESC\n"
232 "LIMIT 1;\n");
233
234 if (!q.exec()) {
235 warnDbMigration << "Could not retrieve version information from the database." << q.lastError();
236 abort();
237 }
238 q.first();
239 schemaVersion = q.value(0).toString();
240 kritaVersion = q.value(1).toString();
241 creationDate = q.value(2).toInt();
242 }
243
244 oldSchemaVersionNumber = QVersionNumber::fromString(schemaVersion);
245 newSchemaVersionNumber = QVersionNumber::fromString(KisResourceCacheDb::databaseVersion);
246
247 if (QVersionNumber::compare(oldSchemaVersionNumber, newSchemaVersionNumber) != 0) {
248
249 infoDbMigration << "Old schema:" << schemaVersion << "New schema:" << newSchemaVersionNumber;
250
251 schemaIsOutDated = true;
253
254 if (newSchemaVersionNumber == QVersionNumber::fromString("0.0.18")
255 && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) >= 0
256 && QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.18")) < 0) {
257
258 bool from14to15 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14");
259
260 bool from15to16 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
261 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15");
262
263 bool from16to17 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
264 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15")
265 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.16");
266
267 bool from17to18 = oldSchemaVersionNumber == QVersionNumber::fromString("0.0.14")
268 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.15")
269 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.16")
270 || oldSchemaVersionNumber == QVersionNumber::fromString("0.0.17");
271
272 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
273
274 bool success = true;
275 if (from14to15) {
276 QSqlError error = runUpdateScript(
277 "ALTER TABLE resource_tags\n"
278 "ADD COLUMN active INTEGER NOT NULL DEFAULT 1",
279 "Update resource tags table (add \'active\' column)");
280 if (error.type() != QSqlError::NoError) {
281 success = false;
282 }
283 }
284 if (success && from15to16) {
285 infoDbMigration << "Going to update indices";
286
287 QStringList indexes = QStringList() << "tags" << "resources" << "tag_translations" << "resource_tags";
288
289 Q_FOREACH(const QString &index, indexes) {
290 QSqlError error = runUpdateScriptFile(":/create_index_" + index + ".sql",
291 QString("Create index for %1").arg(index));
292 if (error.type() != QSqlError::NoError) {
293 success = false;
294 }
295 }
296 }
297
298 if (success && from16to17) {
299 QSqlError error = runUpdateScriptFile(":/create_index_resources_signature.sql",
300 "Create index for resources_signature");
301 if (error.type() != QSqlError::NoError) {
302 success = false;
303 }
304 }
305
306 if (success && from17to18) {
307 {
308 QSqlError error = runUpdateScriptFile(":/0_0_18_0001_cleanup_metadata_table.sql",
309 "Cleanup and deduplicate metadata table");
310 if (error.type() != QSqlError::NoError) {
311 success = false;
312 }
313 }
314 if (success) {
315 QSqlError error = runUpdateScriptFile(":/0_0_18_0002_update_metadata_table_constraints.sql",
316 "Update metadata table constraints");
317 if (error.type() != QSqlError::NoError) {
318 success = false;
319 }
320 }
321 if (success) {
322 QSqlError error = runUpdateScriptFile(":/create_index_metadata_key.sql",
323 "Create index for metadata_key");
324 if (error.type() != QSqlError::NoError) {
325 success = false;
326 }
327 }
328 }
329
330 if (success) {
331 if (!updateSchemaVersion()) {
332 success = false;
333 }
334
335 transactionLock.commit();
336
337 if (success) {
338 QSqlError error = runUpdateScript("VACUUM",
339 "Vacuum database after updating schema");
340 if (error.type() != QSqlError::NoError) {
341 success = false;
342 }
343 }
344 } else {
345 transactionLock.rollback();
346 }
347
348 schemaIsOutDated = !success;
349
350 }
351
352 if (schemaIsOutDated) {
353 QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The resource database scheme has changed. Krita will backup your database and create a new database."));
354 if (QVersionNumber::compare(oldSchemaVersionNumber, QVersionNumber::fromString("0.0.14")) > 0) {
356 }
357 db.close();
358 QFile::remove(location + "/" + KisResourceCacheDb::resourceCacheDbFilename);
359 db.open();
360 }
361 }
362
363 }
364
365 if (allTablesPresent && !schemaIsOutDated) {
366 KisUsageLogger::log(QString("Database is up to date. Version: %1, created by Krita %2, at %3")
367 .arg(schemaVersion)
368 .arg(kritaVersion)
369 .arg(QDateTime::fromSecsSinceEpoch(creationDate).toString()));
370
374
375 return QSqlError();
376 }
377 }
378
379 KisUsageLogger::log(QString("Creating database from scratch (%1, %2).")
380 .arg(oldSchemaVersionNumber.toString().isEmpty() ? QString("database didn't exist") : ("old schema version: " + oldSchemaVersionNumber.toString()))
381 .arg("new schema version: " + newSchemaVersionNumber.toString()));
382
383 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
384
385 // Create tables
386 Q_FOREACH(const QString &table, tables) {
387 QSqlError error =
388 runUpdateScriptFile(":/create_" + table + ".sql", QString("Create table %1").arg(table));
389 if (error.type() != QSqlError::NoError) {
390 return error;
391 }
392 }
393
394 {
395 // metadata table constraints were updated in version 0.0.18
396 QSqlError error = runUpdateScriptFile(":/0_0_18_0002_update_metadata_table_constraints.sql",
397 "Update metadata table constraints");
398
399 if (error.type() != QSqlError::NoError) {
400 return error;
401 }
402 }
403
404 // Create indexes
405 QStringList indexes;
406
407 // these indexes came in version 0.0.16
408 indexes << "storages" << "versioned_resources" << "tags" << "resources" << "tag_translations" << "resource_tags";
409
410 // this index came in version 0.0.17
411 indexes << "resources_signature";
412
413 // this index came in version 0.0.18
414 indexes << "metadata_key";
415
416 Q_FOREACH(const QString &index, indexes) {
417 QSqlError error = runUpdateScriptFile(":/create_index_" + index + ".sql",
418 QString("Create index for %1").arg(index));
419 if (error.type() != QSqlError::NoError) {
420 return error;
421 }
422 }
423
424 // Fill lookup tables
425 {
426 QFile f(":/fill_storage_types.sql");
427 if (f.open(QFile::ReadOnly)) {
428 QString sql = f.readAll();
429 Q_FOREACH(const QString &originType, KisResourceCacheDb::storageTypes) {
430 const QString updateStep = QString("Register storage type: %1").arg(originType);
431 QSqlQuery q(sql);
432 q.addBindValue(originType);
433 if (!q.exec()) {
434 warnDbMigration << "Could execute DB update step:" << updateStep << q.lastError();
435 warnDbMigration << " faulty statement:" << sql;
436 return db.lastError();
437 }
438 infoDbMigration << "Completed DB update step:" << updateStep;
439 }
440 }
441 else {
442 return QSqlError("Error executing SQL", QString("Could not find SQL fill_storage_types.sql."), QSqlError::StatementError);
443 }
444 }
445
446 {
447 QFile f(":/fill_resource_types.sql");
448 if (f.open(QFile::ReadOnly)) {
449 QString sql = f.readAll();
450 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
451 const QString updateStep = QString("Register resource type: %1").arg(resourceType);
452 QSqlQuery q(sql);
453 q.addBindValue(resourceType);
454 if (!q.exec()) {
455 warnDbMigration << "Could execute DB update step:" << updateStep << q.lastError();
456 warnDbMigration << " faulty statement:" << sql;
457 return db.lastError();
458 }
459 infoDbMigration << "Completed DB update step:" << updateStep;
460 }
461 }
462 else {
463 return QSqlError("Error executing SQL", QString("Could not find SQL fill_resource_types.sql."), QSqlError::StatementError);
464 }
465 }
466
467 if (!updateSchemaVersion()) {
468 return QSqlError("Error executing SQL", QString("Could not update schema version."), QSqlError::StatementError);
469 }
470
471 transactionLock.commit();
472
476
477 return QSqlError();
478}
479
480bool KisResourceCacheDb::initialize(const QString &location)
481{
482 QSqlError err = createDatabase(location);
483
484 s_valid = !err.isValid();
485 switch (err.type()) {
486 case QSqlError::NoError:
487 s_lastError = QString();
488 break;
489 case QSqlError::ConnectionError:
490 s_lastError = QString("Could not initialize the resource cache database. Connection error: %1").arg(err.text());
491 break;
492 case QSqlError::StatementError:
493 s_lastError = QString("Could not initialize the resource cache database. Statement error: %1").arg(err.text());
494 break;
495 case QSqlError::TransactionError:
496 s_lastError = QString("Could not initialize the resource cache database. Transaction error: %1").arg(err.text());
497 break;
498 case QSqlError::UnknownError:
499 s_lastError = QString("Could not initialize the resource cache database. Unknown error: %1").arg(err.text());
500 break;
501 }
502
503 // Delete all storages that are no longer known to the resource locator (including the memory storages)
505
506 return s_valid;
507}
508
509std::pair<QVector<int>,QVector<int>> KisResourceCacheDb::tagsForStorage(const QString &resourceType, const QString &storageLocation)
510{
511 try {
512 KisSqlQueryLoader loader(":/sql/storage_tags_ref_count.sql", KisSqlQueryLoader::single_statement_mode);
513 loader.query().bindValue(":resource_type", resourceType);
514 loader.query().bindValue(":location", changeToEmptyIfNull(storageLocation));
515 loader.exec();
516
517 QVector<int> uniqueTags;
518 QVector<int> sharedTags;
519
520 while (loader.query().next()) {
521 if (loader.query().value("ref_count").toInt() > 1) {
522 sharedTags << loader.query().value("tag_id").toInt();
523 } else {
524 uniqueTags << loader.query().value("tag_id").toInt();
525 }
526 }
527
528 return {uniqueTags, sharedTags};
529
530 } catch (const KisSqlQueryLoader::FileException &e) {
531 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
532 qWarning().noquote() << " file:" << e.filePath;
533 qWarning().noquote() << " error:" << e.fileErrorString;
534 return {};
535 } catch (const KisSqlQueryLoader::SQLException &e) {
536 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
537 qWarning().noquote() << " file:" << e.filePath;
538 qWarning().noquote() << " statement:" << e.statementIndex;
539 qWarning().noquote() << " error:" << e.sqlError.text();
540 return {};
541 }
542
543 return {};
544}
545
546QVector<int> KisResourceCacheDb::resourcesForStorage(const QString &resourceType, const QString &storageLocation)
547{
548 QVector<int> result;
549
550 QSqlQuery q;
551
552 if (!q.prepare("SELECT resources.id\n"
553 "FROM resources\n"
554 ", resource_types\n"
555 ", storages\n"
556 "WHERE resources.resource_type_id = resource_types.id\n"
557 "AND storages.id = resources.storage_id\n"
558 "AND storages.location = :storage_location\n"
559 "AND resource_types.name = :resource_type\n")) {
560
561 qWarning() << "Could not read and prepare resourcesForStorage" << q.lastError();
562 return result;
563 }
564
565 q.bindValue(":resource_type", resourceType);
566 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
567
568 if (!q.exec()) {
569 qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
570 return result;
571 }
572
573 while (q.next()) {
574 result << q.value(0).toInt();
575 }
576
577 return result;
578}
579
580int KisResourceCacheDb::resourceIdForResource(const QString &resourceFileName, const QString &resourceType, const QString &storageLocation)
581{
582 //qDebug() << "resourceIdForResource" << resourceName << resourceFileName << resourceType << storageLocation;
583
584 QSqlQuery q;
585
586 if (!q.prepare("SELECT resources.id\n"
587 "FROM resources\n"
588 ", resource_types\n"
589 ", storages\n"
590 "WHERE resources.resource_type_id = resource_types.id\n"
591 "AND storages.id = resources.storage_id\n"
592 "AND storages.location = :storage_location\n"
593 "AND resource_types.name = :resource_type\n"
594 "AND resources.filename = :filename\n")) {
595 qWarning() << "Could not read and prepare resourceIdForResource" << q.lastError();
596 return -1;
597 }
598
599 q.bindValue(":filename", resourceFileName);
600 q.bindValue(":resource_type", resourceType);
601 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
602
603 if (!q.exec()) {
604 qWarning() << "Could not query resourceIdForResource" << q.boundValues() << q.lastError();
605 return -1;
606 }
607
608 if (q.first()) {
609 return q.value(0).toInt();
610 }
611
612 // couldn't be found in the `resources` table, but can still be in versioned_resources
613
614 if (!q.prepare("SELECT versioned_resources.resource_id\n"
615 "FROM resources\n"
616 ", resource_types\n"
617 ", versioned_resources\n"
618 ", storages\n"
619 "WHERE resources.resource_type_id = resource_types.id\n" // join resources and resource_types by resource id
620 "AND versioned_resources.resource_id = resources.id\n" // join versioned_resources and resources by resource id
621 "AND storages.id = versioned_resources.storage_id\n" // join storages and versioned_resources by storage id
622 "AND storages.location = :storage_location\n" // storage location must be the same as asked for
623 "AND resource_types.name = :resource_type\n" // resource type must be the same as asked for
624 "AND versioned_resources.filename = :filename\n")) { // filename must be the same as asked for
625 qWarning() << "Could not read and prepare resourceIdForResource (in versioned resources)" << q.lastError();
626 return -1;
627 }
628
629 q.bindValue(":filename", resourceFileName);
630 q.bindValue(":resource_type", resourceType);
631 q.bindValue(":storage_location", changeToEmptyIfNull(storageLocation));
632
633 if (!q.exec()) {
634 qWarning() << "Could not query resourceIdForResource (in versioned resources)" << q.boundValues() << q.lastError();
635 return -1;
636 }
637
638 if (q.first()) {
639 return q.value(0).toInt();
640 }
641
642 // commenting out, because otherwise it spams the console on every new resource in the local resources folder
643 // qWarning() << "Could not find resource" << resourceName << resourceFileName << resourceType << storageLocation;
644 return -1;
645
646}
647
648bool KisResourceCacheDb::resourceNeedsUpdating(int resourceId, QDateTime timestamp)
649{
650 QSqlQuery q;
651 if (!q.prepare("SELECT timestamp\n"
652 "FROM versioned_resources\n"
653 "WHERE resource_id = :resource_id\n"
654 "AND version = (SELECT MAX(version)\n"
655 " FROM versioned_resources\n"
656 " WHERE resource_id = :resource_id);")) {
657 qWarning() << "Could not prepare resourceNeedsUpdating statement" << q.lastError();
658 return false;
659 }
660
661 q.bindValue(":resource_id", resourceId);
662
663 if (!q.exec()) {
664 qWarning() << "Could not query for the most recent timestamp" << q.boundValues() << q.lastError();
665 return false;
666 }
667
668 if (!q.first()) {
669 qWarning() << "Inconsistent database: could not find a version for resource with Id" << resourceId;
670 return false;
671 }
672
673 QVariant resourceTimeStamp = q.value(0);
674
675 if (!resourceTimeStamp.isValid()) {
676 qWarning() << "Could not retrieve timestamp from versioned_resources" << resourceId;
677 return false;
678 }
679
680 return (timestamp.toSecsSinceEpoch() > resourceTimeStamp.toInt());
681}
682
683bool KisResourceCacheDb::addResourceVersion(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
684{
685 bool r = false;
686
687
688 r = addResourceVersionImpl(resourceId, timestamp, storage, resource);
689
690 if (!r) return r;
691
692 r = makeResourceTheCurrentVersion(resourceId, resource);
693
694 return r;
695}
696
697bool KisResourceCacheDb::addResourceVersionImpl(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
698{
699 bool r = false;
700
701 // Create the new version. The resource is expected to have an updated version number, or
702 // this will fail on the unique index on resource_id, storage_id and version.
703 //
704 // This function **only** adds to the versioned_resources table.
705 // The resources table should be updated by the caller manually using
706 // updateResourceTableForResourceIfNeeded()
707
708 Q_ASSERT(resource->version() >= 0);
709
710 QSqlQuery q;
711 r = q.prepare("INSERT INTO versioned_resources \n"
712 "(resource_id, storage_id, version, filename, timestamp, md5sum)\n"
713 "VALUES\n"
714 "( :resource_id\n"
715 ", (SELECT id \n"
716 " FROM storages \n"
717 " WHERE location = :storage_location)\n"
718 ", :version\n"
719 ", :filename\n"
720 ", :timestamp\n"
721 ", :md5sum\n"
722 ");");
723
724 if (!r) {
725 qWarning() << "Could not prepare addResourceVersion statement" << q.lastError();
726 return r;
727 }
728
729 q.bindValue(":resource_id", resourceId);
730 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
731 q.bindValue(":version", resource->version());
732 q.bindValue(":filename", resource->filename());
733 q.bindValue(":timestamp", timestamp.toSecsSinceEpoch());
734 KIS_SAFE_ASSERT_RECOVER_NOOP(!resource->md5Sum().isEmpty());
735 q.bindValue(":md5sum", resource->md5Sum());
736 r = q.exec();
737 if (!r) {
738
739 qWarning() << "Could not execute addResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << resource->name() << resource->filename() << "version" << resource->version();
740 return r;
741 }
742
743 return r;
744}
745
747{
748 bool r = false;
749
750 // Remove a version of the resource. This function **only** removes data from
751 // the versioned_resources table. The resources table should be updated by
752 // the caller manually using updateResourceTableForResourceIfNeeded()
753
754 QSqlQuery q;
755 r = q.prepare("DELETE FROM versioned_resources \n"
756 "WHERE resource_id = :resource_id\n"
757 "AND version = :version\n"
758 "AND storage_id = (SELECT id \n"
759 " FROM storages \n"
760 " WHERE location = :storage_location);");
761
762 if (!r) {
763 qWarning() << "Could not prepare removeResourceVersionImpl statement" << q.lastError();
764 return r;
765 }
766
767 q.bindValue(":resource_id", resourceId);
768 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
769 q.bindValue(":version", version);
770 r = q.exec();
771 if (!r) {
772
773 qWarning() << "Could not execute removeResourceVersionImpl statement" << q.lastError() << resourceId << storage->name() << storage->location() << "version" << version;
774 return r;
775 }
776
777 return r;
778}
779
780bool KisResourceCacheDb::updateResourceTableForResourceIfNeeded(int resourceId, const QString &resourceType, KisResourceStorageSP storage)
781{
782 bool r = false;
783
784 int maxVersion = -1;
785 {
786 QSqlQuery q;
787 r = q.prepare("SELECT MAX(version)\n"
788 "FROM versioned_resources\n"
789 "WHERE resource_id = :resource_id;");
790 if (!r) {
791 qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
792 return r;
793 }
794
795 q.bindValue(":resource_id", resourceId);
796
797 r = q.exec();
798 if (!r) {
799 qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
800 return r;
801 }
802
803 r = q.first();
805
806 maxVersion = q.value(0).toInt();
807 }
808
809 QString maxVersionFilename;
810 {
811 QSqlQuery q;
812 r = q.prepare("SELECT filename\n"
813 "FROM versioned_resources\n"
814 "WHERE resource_id = :resource_id\n"
815 "AND version = :version;");
816 if (!r) {
817 qWarning() << "Could not prepare findMaxVersionFilename statement" << q.lastError();
818 return r;
819 }
820
821 q.bindValue(":resource_id", resourceId);
822 q.bindValue(":version", maxVersion);
823
824 r = q.exec();
825 if (!r) {
826 qWarning() << "Could not execute findMaxVersionFilename query" << q.boundValues() << q.lastError();
827 return r;
828 }
829
830 if (!q.first()) {
831 return removeResourceCompletely(resourceId);
832 } else {
833 maxVersionFilename = q.value(0).toString();
834 }
835 }
836
837 QString currentFilename;
838 {
839 QSqlQuery q;
840 r = q.prepare("SELECT filename\n"
841 "FROM resources\n"
842 "WHERE id = :resource_id;");
843 if (!r) {
844 qWarning() << "Could not prepare findMaxVersion statement" << q.lastError();
845 return r;
846 }
847
848 q.bindValue(":resource_id", resourceId);
849
850 r = q.exec();
851 if (!r) {
852 qWarning() << "Could not execute findMaxVersion query" << q.boundValues() << q.lastError();
853 return r;
854 }
855
856 r = q.first();
858
859 currentFilename = q.value(0).toString();
860 }
861
862 if (currentFilename != maxVersionFilename) {
863 const QString url = resourceType + "/" + maxVersionFilename;
864 KoResourceSP resource = storage->resource(url);
866 resource->setVersion(maxVersion);
867 resource->setMD5Sum(storage->resourceMd5(url));
868 resource->setStorageLocation(storage->location());
869 r = makeResourceTheCurrentVersion(resourceId, resource);
870 }
871
872 return r;
873}
874
876{
877 bool r = false;
878
879 QSqlQuery q;
880 r = q.prepare("UPDATE resources\n"
881 "SET name = :name\n"
882 ", filename = :filename\n"
883 ", tooltip = :tooltip\n"
884 ", thumbnail = :thumbnail\n"
885 ", status = 1\n"
886 ", md5sum = :md5sum\n"
887 "WHERE id = :id");
888 if (!r) {
889 qWarning() << "Could not prepare updateResource statement" << q.lastError();
890 return r;
891 }
892
893 q.bindValue(":name", resource->name());
894 q.bindValue(":filename", resource->filename());
895 q.bindValue(":tooltip", i18n(resource->name().toUtf8()));
896 q.bindValue(":md5sum", resource->md5Sum());
897
898 QBuffer buf;
899 buf.open(QBuffer::WriteOnly);
900 resource->thumbnail().save(&buf, "PNG");
901 buf.close();
902 q.bindValue(":thumbnail", buf.data());
903 q.bindValue(":id", resourceId);
904
905 r = q.exec();
906 if (!r) {
907 qWarning() << "Could not update resource" << q.boundValues() << q.lastError();
908 }
909
910 if (!resource->metadata().isEmpty()) {
911 return updateMetaDataForId(resource->metadata(), resourceId, METADATA_RESOURCES);
912 }
913
914 return r;
915}
916
918{
919 bool r = false;
920
921 {
922 QSqlQuery q;
923 r = q.prepare("DELETE FROM versioned_resources \n"
924 "WHERE resource_id = :resource_id;");
925
926 if (!r) {
927 qWarning() << "Could not prepare removeResourceCompletely1 statement" << q.lastError();
928 return r;
929 }
930
931 q.bindValue(":resource_id", resourceId);
932 r = q.exec();
933 if (!r) {
934 qWarning() << "Could not execute removeResourceCompletely1 statement" << q.lastError() << resourceId;
935 return r;
936 }
937 }
938
939 {
940 QSqlQuery q;
941 r = q.prepare("DELETE FROM resources \n"
942 "WHERE id = :resource_id;");
943
944 if (!r) {
945 qWarning() << "Could not prepare removeResourceCompletely2 statement" << q.lastError();
946 return r;
947 }
948
949 q.bindValue(":resource_id", resourceId);
950 r = q.exec();
951 if (!r) {
952 qWarning() << "Could not execute removeResourceCompletely2 statement" << q.lastError() << resourceId;
953 return r;
954 }
955 }
956
957 {
958 QSqlQuery q;
959 r = q.prepare("DELETE FROM resource_tags \n"
960 "WHERE resource_id = :resource_id;");
961
962 if (!r) {
963 qWarning() << "Could not prepare removeResourceCompletely3 statement" << q.lastError();
964 return r;
965 }
966
967 q.bindValue(":resource_id", resourceId);
968 r = q.exec();
969 if (!r) {
970 qWarning() << "Could not execute removeResourceCompletely3 statement" << q.lastError() << resourceId;
971 return r;
972 }
973 }
974
975 {
976 QSqlQuery q;
977 r = q.prepare("DELETE FROM metadata \n"
978 "WHERE foreign_id = :resource_id\n"
979 "AND table_name = :table;");
980
981 if (!r) {
982 qWarning() << "Could not prepare removeResourceCompletely4 statement" << q.lastError();
983 return r;
984 }
985
986 q.bindValue(":resource_id", resourceId);
987 q.bindValue(":table", METADATA_RESOURCES);
988 r = q.exec();
989 if (!r) {
990 qWarning() << "Could not execute removeResourceCompletely4 statement" << q.lastError() << resourceId;
991 return r;
992 }
993 }
994
995 return r;
996}
997
998bool KisResourceCacheDb::getResourceIdFromFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
999{
1000 QSqlQuery q;
1001
1002 bool r = q.prepare("SELECT resources.id FROM resources\n"
1003 ", resource_types\n"
1004 ", storages\n"
1005 "WHERE resources.filename = :filename\n" // bind to filename
1006 "AND resource_types.id = resources.resource_type_id\n" // join resources_types + resources
1007 "AND resource_types.name = :resourceType\n" // bind to resource type
1008 "AND resources.storage_id = storages.id\n" // join resources + storages
1009 "AND storages.location = :storageLocation"); // bind to storage location
1010
1011 if (!r) {
1012 qWarning() << "Could not prepare getResourceIdFromFilename statement" << q.lastError() << q.executedQuery();
1013 return r;
1014 }
1015
1016 q.bindValue(":filename", filename);
1017 q.bindValue(":resourceType", resourceType);
1018 q.bindValue(":storageLocation", changeToEmptyIfNull(storageLocation));
1019
1020 r = q.exec();
1021 if (!r) {
1022 qWarning() << "Could not execute getResourceIdFromFilename statement" << q.lastError() << filename << resourceType;
1023 return r;
1024 }
1025
1026 r = q.first();
1027 if (r) {
1028 outResourceId = q.value("resources.id").toInt();
1029 }
1030
1031 return r;
1032}
1033
1034bool KisResourceCacheDb::getResourceIdFromVersionedFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
1035{
1036 QSqlQuery q;
1037
1038 bool r = q.prepare("SELECT resource_id FROM versioned_resources\n"
1039 ", resources\n"
1040 ", resource_types\n"
1041 ", storages\n"
1042 "WHERE versioned_resources.filename = :filename\n" // bind to filename
1043 "AND resources.id = versioned_resources.resource_id\n" // join resources + versioned_resources
1044 "AND resource_types.id = resources.resource_type_id\n" // join resources_types + resources
1045 "AND resource_types.name = :resourceType\n" // bind to resource type
1046 "AND resources.storage_id = storages.id\n" // join resources + storages
1047 "AND storages.location = :storageLocation"); // bind to storage location
1048
1049 if (!r) {
1050 qWarning() << "Could not prepare getResourceIdFromVersionedFilename statement" << q.lastError() << q.executedQuery();
1051 return r;
1052 }
1053
1054
1055 q.bindValue(":filename", filename);
1056 q.bindValue(":resourceType", resourceType);
1057 q.bindValue(":storageLocation", changeToEmptyIfNull(storageLocation));
1058
1059 r = q.exec();
1060 if (!r) {
1061 qWarning() << "Could not execute getResourceIdFromVersionedFilename statement" << q.lastError() << filename << resourceType;
1062 return r;
1063 }
1064
1065 r = q.first();
1066 if (r) {
1067 outResourceId = q.value("resource_id").toInt();
1068 }
1069
1070 return r;
1071}
1072
1073bool KisResourceCacheDb::getAllVersionsLocations(int resourceId, QStringList &outVersionsLocationsList)
1074{
1075 QSqlQuery q;
1076 bool r = q.prepare("SELECT filename FROM versioned_resources \n"
1077 "WHERE resource_id = :resource_id;");
1078
1079 if (!r) {
1080 qWarning() << "Could not prepare getAllVersionsLocations statement" << q.lastError();
1081 return r;
1082 }
1083
1084 q.bindValue(":resource_id", resourceId);
1085 r = q.exec();
1086 if (!r) {
1087 qWarning() << "Could not execute getAllVersionsLocations statement" << q.lastError() << resourceId;
1088 return r;
1089 }
1090
1091 outVersionsLocationsList = QStringList();
1092 while (q.next()) {
1093 outVersionsLocationsList << q.value("filename").toString();
1094 }
1095
1096 return r;
1097
1098}
1099
1100bool KisResourceCacheDb::addResource(KisResourceStorageSP storage, QDateTime timestamp, KoResourceSP resource, const QString &resourceType)
1101{
1102 bool r = false;
1103
1104 if (!s_valid) {
1105 qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
1106 return false;
1107 }
1108
1109 if (!resource || !resource->valid()) {
1110 qWarning() << "KisResourceCacheDb::addResource: The resource is not valid:" << resource->filename();
1111 // We don't care about invalid resources and will just ignore them.
1112 return true;
1113 }
1114 bool temporary = (storage->type() == KisResourceStorage::StorageType::Memory);
1115
1116 // Check whether it already exists
1117 int resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1118 if (resourceId > -1) {
1119 return true;
1120 }
1121
1122 QSqlQuery q;
1123 r = q.prepare("INSERT INTO resources \n"
1124 "(storage_id, resource_type_id, name, filename, tooltip, thumbnail, status, temporary, md5sum) \n"
1125 "VALUES \n"
1126 "((SELECT id "
1127 " FROM storages "
1128 " WHERE location = :storage_location)\n"
1129 ", (SELECT id\n"
1130 " FROM resource_types\n"
1131 " WHERE name = :resource_type)\n"
1132 ", :name\n"
1133 ", :filename\n"
1134 ", :tooltip\n"
1135 ", :thumbnail\n"
1136 ", :status\n"
1137 ", :temporary\n"
1138 ", :md5sum)");
1139
1140 if (!r) {
1141 qWarning() << "Could not prepare addResource statement" << q.lastError();
1142 return r;
1143 }
1144
1145 q.bindValue(":resource_type", resourceType);
1146 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1147 q.bindValue(":name", resource->name());
1148 q.bindValue(":filename", resource->filename());
1149
1150 QString translationContext;
1151 if (storage->type() == KisResourceStorage::StorageType::Bundle) {
1152 translationContext = "./krita/data/bundles/" + KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())
1153 + ":" + resourceType + "/" + resource->filename();
1154 } else if (storage->location() == "memory") {
1155 translationContext = "memory/" + resourceType + "/" + resource->filename();
1156 }
1157 else if (resource->filename().endsWith(".myb", Qt::CaseInsensitive)) {
1158 translationContext = "./plugins/paintops/mypaint/brushes/" + resource->filename();
1159 } else {
1160 translationContext = "./krita/data/" + resourceType + "/" + resource->filename();
1161 }
1162
1163 {
1164 QByteArray ctx = translationContext.toUtf8();
1165 QString translatedName = i18nc(ctx, resource->name().toUtf8());
1166 if (translatedName == resource->name()) {
1167 // Try using the file name without the file extension, and replaces '_' with spaces.
1168 QString altName = QFileInfo(resource->filename()).completeBaseName().replace('_', ' ');
1169 QString altTranslatedName = i18nc(ctx, altName.toUtf8());
1170 if (altName != altTranslatedName) {
1171 translatedName = altTranslatedName;
1172 }
1173 }
1174 q.bindValue(":tooltip", translatedName);
1175 }
1176
1177 QBuffer buf;
1178 buf.open(QBuffer::WriteOnly);
1179 resource->image().save(&buf, "PNG");
1180 buf.close();
1181 q.bindValue(":thumbnail", buf.data());
1182
1183 q.bindValue(":status", resource->active());
1184 q.bindValue(":temporary", (temporary ? 1 : 0));
1185 q.bindValue(":md5sum", resource->md5Sum());
1186
1187 r = q.exec();
1188 if (!r) {
1189 qWarning() << "Could not execute addResource statement" << q.lastError() << q.boundValues();
1190 return r;
1191 }
1192 resourceId = resourceIdForResource(resource->filename(), resourceType, KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1193
1194 if (resourceId < 0) {
1195
1196 qWarning() << "Adding to database failed, resource id after adding is " << resourceId << "! (Probable reason: the resource has the same filename, storage, resource type as an existing resource). Resource is: "
1197 << resource->name()
1198 << resource->filename()
1199 << resourceType
1201 return false;
1202 }
1203
1204 resource->setResourceId(resourceId);
1205
1206 if (!addResourceVersionImpl(resourceId, timestamp, storage, resource)) {
1207 qWarning() << "Could not add resource version" << resource;
1208 return false;
1209 }
1210
1211 if (!resource->metadata().isEmpty()) {
1212 return updateMetaDataForId(resource->metadata(), resource->resourceId(), METADATA_RESOURCES);
1213 }
1214
1215 return true;
1216
1217
1218}
1219
1221{
1222 QSqlDatabase::database().transaction();
1223 QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1224 while (iter->hasNext()) {
1225 iter->next();
1226
1228 iter->versions();
1229
1230 int resourceId = -1;
1231
1232 while (verIt->hasNext()) {
1233 verIt->next();
1234
1235 KoResourceSP resource = verIt->resource();
1236 if (resource && resource->valid()) {
1237 resource->setVersion(verIt->guessedVersion());
1238 resource->setMD5Sum(storage->resourceMd5(verIt->url()));
1239
1240 if (resourceId < 0) {
1241 if (addResource(storage, iter->lastModified(), resource, iter->type())) {
1242 resourceId = resource->resourceId();
1243 } else {
1244 qWarning() << "Could not add resource" << resource->filename() << "to the database";
1245 }
1246 } else {
1247 if (!addResourceVersion(resourceId, iter->lastModified(), storage, resource)) {
1248 qWarning() << "Could not add resource version" << resource->filename() << "to the database";
1249 }
1250 }
1251 }
1252 }
1253 }
1254 QSqlDatabase::database().commit();
1255 return true;
1256}
1257
1258bool KisResourceCacheDb::setResourceActive(int resourceId, bool active)
1259{
1260 if (resourceId < 0) {
1261 qWarning() << "Invalid resource id; cannot remove resource";
1262 return false;
1263 }
1264 QSqlQuery q;
1265 bool r = q.prepare("UPDATE resources\n"
1266 "SET status = :status\n"
1267 "WHERE id = :resource_id");
1268 if (!r) {
1269 qWarning() << "Could not prepare removeResource query" << q.lastError();
1270 }
1271 q.bindValue(":status", active);
1272 q.bindValue(":resource_id", resourceId);
1273 if (!q.exec()) {
1274 qWarning() << "Could not update resource" << resourceId << "to inactive" << q.lastError();
1275 return false;
1276 }
1277
1278 return true;
1279}
1280
1281bool KisResourceCacheDb::tagResource(const QString &resourceFileName, KisTagSP tag, const QString &resourceType)
1282{
1283 // Get tag id
1284 int tagId {-1};
1285 {
1286 QFile f(":/select_tag.sql");
1287 if (f.open(QFile::ReadOnly)) {
1288 QSqlQuery q;
1289 if (!q.prepare(f.readAll())) {
1290 qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1291 return false;
1292 }
1293 q.bindValue(":url", tag->url());
1294 q.bindValue(":resource_type", resourceType);
1295
1296 if (!q.exec()) {
1297 qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1298 return false;
1299 }
1300
1301 if (!q.first()) {
1302 qWarning() << "Could not find tag" << q.boundValues() << q.lastError();
1303 return false;
1304 }
1305
1306 tagId = q.value(0).toInt();
1307 }
1308 }
1309
1310
1311 // Get resource id
1312 QSqlQuery q;
1313 bool r = q.prepare("SELECT resources.id\n"
1314 "FROM resources\n"
1315 ", resource_types\n"
1316 "WHERE resources.resource_type_id = resource_types.id\n"
1317 "AND resource_types.name = :resource_type\n"
1318 "AND resources.filename = :resource_filename\n");
1319 if (!r) {
1320 qWarning() << "Could not prepare tagResource query" << q.lastError();
1321 return false;
1322 }
1323
1324 q.bindValue(":resource_type", resourceType);
1325 q.bindValue(":resource_filename", resourceFileName);
1326
1327 if (!q.exec()) {
1328 qWarning() << "Could not execute tagResource statement" << q.boundValues() << q.lastError();
1329 return false;
1330 }
1331
1332
1333 while (q.next()) {
1334
1335 int resourceId = q.value(0).toInt();
1336
1337 if (resourceId < 0) {
1338 qWarning() << "Could not find resource to tag" << resourceFileName << resourceType;
1339 continue;
1340 }
1341
1342 {
1343 QSqlQuery q;
1344 if (!q.prepare("SELECT COUNT(*)\n"
1345 "FROM resource_tags\n"
1346 "WHERE resource_id = :resource_id\n"
1347 "AND tag_id = :tag_id")) {
1348 qWarning() << "Could not prepare tagResource query 2" << q.lastError();
1349 continue;
1350 }
1351 q.bindValue(":resource_id", resourceId);
1352 q.bindValue(":tag_id", tagId);
1353
1354 if (!q.exec()) {
1355 qWarning() << "Could not execute tagResource query 2" << q.lastError() << q.boundValues();
1356 continue;
1357 }
1358
1359 q.first();
1360 int count = q.value(0).toInt();
1361 if (count > 0) {
1362 continue;
1363 }
1364 }
1365
1366 {
1367 QSqlQuery q;
1368 if (!q.prepare("INSERT INTO resource_tags\n"
1369 "(resource_id, tag_id)\n"
1370 "VALUES\n"
1371 "(:resource_id, :tag_id);")) {
1372 qWarning() << "Could not prepare tagResource insert statement" << q.lastError();
1373 continue;
1374 }
1375
1376 q.bindValue(":resource_id", resourceId);
1377 q.bindValue(":tag_id", tagId);
1378
1379 if (!q.exec()) {
1380 qWarning() << "Could not execute tagResource stagement" << q.boundValues() << q.lastError();
1381 continue;
1382 }
1383 }
1384 }
1385 return true;
1386}
1387
1388bool KisResourceCacheDb::hasTag(const QString &url, const QString &resourceType)
1389{
1390 QFile f(":/select_tag.sql");
1391 if (f.open(QFile::ReadOnly)) {
1392 QSqlQuery q;
1393 if (!q.prepare(f.readAll())) {
1394 qWarning() << "Could not read and prepare select_tag.sql" << q.lastError();
1395 return false;
1396 }
1397 q.bindValue(":url", url);
1398 q.bindValue(":resource_type", resourceType);
1399 if (!q.exec()) {
1400 qWarning() << "Could not query tags" << q.boundValues() << q.lastError();
1401 }
1402 return q.first();
1403 }
1404 qWarning() << "Could not open select_tag.sql";
1405 return false;
1406}
1407
1408bool KisResourceCacheDb::linkTagToStorage(const QString &url, const QString &resourceType, const QString &storageLocation)
1409{
1410 QSqlQuery q;
1411 if (!q.prepare("INSERT INTO tags_storages\n"
1412 "(tag_id, storage_id)\n"
1413 "VALUES\n"
1414 "(\n"
1415 " ( SELECT id\n"
1416 " FROM tags\n"
1417 " WHERE url = :url\n"
1418 " AND resource_type_id = (SELECT id \n"
1419 " FROM resource_types\n"
1420 " WHERE name = :resource_type)"
1421 " )\n"
1422 ",( SELECT id\n"
1423 " FROM storages\n"
1424 " WHERE location = :storage_location\n"
1425 " )\n"
1426 ");")) {
1427 qWarning() << "Could not prepare add tag/storage statement" << q.lastError();
1428 return false;
1429 }
1430
1431 q.bindValue(":url", url);
1432 q.bindValue(":resource_type", resourceType);
1433 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storageLocation)));
1434
1435 if (!q.exec()) {
1436 qWarning() << "Could not insert tag/storage link" << q.boundValues() << q.lastError();
1437 return false;
1438 }
1439 return true;
1440}
1441
1442
1443bool KisResourceCacheDb::addTag(const QString &resourceType, const QString storageLocation, KisTagSP tag)
1444{
1445 if (hasTag(tag->url(), resourceType)) {
1446 // Check whether this storage is already registered for this tag
1447 QSqlQuery q;
1448 if (!q.prepare("SELECT storages.location\n"
1449 "FROM tags_storages\n"
1450 ", tags\n"
1451 ", storages\n"
1452 "WHERE tags.id = tags_storages.tag_id\n"
1453 "AND storages.id = tags_storages.storage_id\n"
1454 "AND tags.resource_type_id = (SELECT id\n"
1455 " FROM resource_types\n"
1456 " WHERE name = :resource_type)\n"
1457 "AND storages.location = :storage_location\n"
1458 "AND tags.url = :url"))
1459 {
1460 qWarning() << "Could not prepare select tags from tags_storages query" << q.lastError();
1461 }
1462
1463 q.bindValue(":url", tag->url());
1464 q.bindValue(":resource_type", resourceType);
1465 q.bindValue(":storage_location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storageLocation)));
1466
1467 if (!q.exec()) {
1468 qWarning() << "Could not execute tags_storages query" << q.boundValues() << q.lastError();
1469 }
1470
1471 // If this tag is not yet linked to the storage, link it
1472 if (!q.first()) {
1473 return linkTagToStorage(tag->url(), resourceType, storageLocation);
1474 }
1475
1476 return true;
1477 }
1478
1479 int tagId;
1480
1481 // Insert the tag
1482 {
1483 QSqlQuery q;
1484 if (!q.prepare("INSERT INTO tags\n"
1485 "(url, name, comment, resource_type_id, active, filename)\n"
1486 "VALUES\n"
1487 "( :url\n"
1488 ", :name\n"
1489 ", :comment\n"
1490 ", (SELECT id\n"
1491 " FROM resource_types\n"
1492 " WHERE name = :resource_type)\n"
1493 ", 1\n"
1494 ", :filename\n"
1495 ");")) {
1496 qWarning() << "Could not prepare insert tag statement" << q.lastError();
1497 return false;
1498 }
1499
1500 q.bindValue(":url", tag->url());
1501 q.bindValue(":name", tag->name(false));
1502 q.bindValue(":comment", tag->comment(false));
1503 q.bindValue(":resource_type", resourceType);
1504 q.bindValue(":filename", tag->filename());
1505
1506 if (!q.exec()) {
1507 qWarning() << "Could not insert tag" << q.boundValues() << q.lastError();
1508 }
1509
1510 tagId = q.lastInsertId().toInt();
1511 }
1512
1513 {
1514 Q_FOREACH(const QString language, tag->names().keys()) {
1515
1516 QString name = tag->names()[language];
1517 QString comment = name;
1518 if (tag->comments().contains(language)) {
1519 comment = tag->comments()[language];
1520 }
1521
1522 QSqlQuery q;
1523 if (!q.prepare("INSERT INTO tag_translations\n"
1524 "( tag_id\n"
1525 ", language\n"
1526 ", name\n"
1527 ", comment\n"
1528 ")\n"
1529 "VALUES\n"
1530 "( :id\n"
1531 ", :language\n"
1532 ", :name\n"
1533 ", :comment\n"
1534 ");")) {
1535 qWarning() << "Could not prepare insert tag_translation query" << q.lastError();
1536 }
1537
1538 q.bindValue(":id", tagId);
1539 q.bindValue(":language", language);
1540 q.bindValue(":name", name);
1541 q.bindValue(":comment", comment);
1542
1543 if (!q.exec()) {
1544 qWarning() << "Could not execute insert tag_translation query" << q.lastError() << q.boundValues();
1545 }
1546 }
1547
1548 }
1549
1550
1551 linkTagToStorage(tag->url(), resourceType, storageLocation);
1552
1553 return true;
1554}
1555
1556bool KisResourceCacheDb::addTags(KisResourceStorageSP storage, QString resourceType)
1557{
1558 QSqlDatabase::database().transaction();
1559 QSharedPointer<KisResourceStorage::TagIterator> iter = storage->tags(resourceType);
1560 while(iter->hasNext()) {
1561 iter->next();
1562 KisTagSP tag = iter->tag();
1563 if (tag && tag->valid()) {
1564 if (!addTag(resourceType, storage->location(), tag)) {
1565 qWarning() << "Could not add tag" << tag << "to the database";
1566 continue;
1567 }
1568 if (!tag->defaultResources().isEmpty()) {
1569 Q_FOREACH(const QString &resourceFileName, tag->defaultResources()) {
1570 if (!tagResource(resourceFileName, tag, resourceType)) {
1571 qWarning() << "Could not tag resource" << QFileInfo(resourceFileName).baseName() << "from" << storage->name() << "filename" << resourceFileName << "with tag" << iter->tag();
1572 }
1573 }
1574 }
1575 }
1576 }
1577 QSqlDatabase::database().commit();
1578 return true;
1579}
1580
1582{
1583 // Check whether the type already exists
1584 const QString name = KisResourceStorage::storageTypeToUntranslatedString(storageType);
1585
1586 {
1587 QSqlQuery q;
1588 if (!q.prepare("SELECT count(*)\n"
1589 "FROM storage_types\n"
1590 "WHERE name = :storage_type\n")) {
1591 qWarning() << "Could not prepare select from storage_types query" << q.lastError();
1592 return false;
1593 }
1594 q.bindValue(":storage_type", name);
1595 if (!q.exec()) {
1596 qWarning() << "Could not execute select from storage_types query" << q.lastError();
1597 return false;
1598 }
1599 q.first();
1600 int rowCount = q.value(0).toInt();
1601 if (rowCount > 0) {
1602 return true;
1603 }
1604 }
1605 // if not, add it
1606 QFile f(":/fill_storage_types.sql");
1607 if (f.open(QFile::ReadOnly)) {
1608 QString sql = f.readAll();
1609 QSqlQuery q(sql);
1610 q.addBindValue(name);
1611 if (!q.exec()) {
1612 qWarning() << "Could not insert" << name << q.lastError();
1613 return false;
1614 }
1615 return true;
1616 }
1617 qWarning() << "Could not open fill_storage_types.sql";
1618 return false;
1619}
1620
1622{
1623 bool r = true;
1624
1625 if (!s_valid) {
1626 qWarning() << "The database is not valid";
1627 return false;
1628 }
1629
1630 {
1631 QSqlQuery q;
1632 r = q.prepare("SELECT * FROM storages WHERE location = :location");
1633 q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1634 r = q.exec();
1635 if (!r) {
1636 qWarning() << "Could not select from storages";
1637 return r;
1638 }
1639 if (q.first()) {
1640 debugResource << "Storage already exists" << storage;
1641 return true;
1642 }
1643 }
1644
1645 // Insert the storage;
1646 {
1647 QSqlQuery q;
1648
1649 r = q.prepare("INSERT INTO storages\n "
1650 "(storage_type_id, location, timestamp, pre_installed, active, thumbnail)\n"
1651 "VALUES\n"
1652 "(:storage_type_id, :location, :timestamp, :pre_installed, :active, :thumbnail);");
1653
1654 if (!r) {
1655 qWarning() << "Could not prepare query" << q.lastError();
1656 return r;
1657 }
1658
1659 const QString sanitizedStorageLocation =
1660 changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1661
1662 q.bindValue(":storage_type_id", static_cast<int>(storage->type()));
1663 q.bindValue(":location", sanitizedStorageLocation);
1664 q.bindValue(":timestamp", storage->timestamp().toSecsSinceEpoch());
1665 q.bindValue(":pre_installed", preinstalled ? 1 : 0);
1666 q.bindValue(":active", !disabledBundles.contains(storage->name()));
1667
1668 QBuffer buf;
1669 buf.open(QBuffer::WriteOnly);
1670 storage->thumbnail().save(&buf, "PNG");
1671 buf.close();
1672 q.bindValue(":thumbnail", buf.data());
1673
1674 r = q.exec();
1675
1676 if (!r) qWarning() << "Could not execute query" << q.lastError();
1677
1678 if (!q.prepare("SELECT id\n"
1679 "FROM storages\n"
1680 "WHERE location = :location\n")) {
1681 qWarning() << "Could not prepare storage id statement" << q.lastError();
1682 }
1683
1684 q.bindValue(":location", sanitizedStorageLocation);
1685 if (!q.exec()) {
1686 qWarning() << "Could not execute storage id statement" << q.boundValues() << q.lastError();
1687 }
1688
1689 if (!q.first()) {
1690 qWarning() << "Could not find id for the newly added storage" << q.lastError();
1691 } else {
1692 storage->setStorageId(q.value("id").toInt());
1693 }
1694 }
1695
1696 // Insert the metadata
1697 {
1698 QStringList keys = storage->metaDataKeys();
1699 if (keys.size() > 0 && storage->storageId() >= 0) {
1700
1701 QMap<QString, QVariant> metadata;
1702
1703 Q_FOREACH(const QString &key, storage->metaDataKeys()) {
1704 metadata[key] = storage->metaData(key);
1705 }
1706
1707 updateMetaDataForId(metadata, storage->storageId(), METADATA_STORAGES);
1708 }
1709 }
1710
1711 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1712 if (!KisResourceCacheDb::addResources(storage, resourceType)) {
1713 qWarning() << "Failed to add all resources for storage" << storage;
1714 r = false;
1715 }
1716 }
1717
1718 return r;
1719}
1720
1722{
1723
1724 bool r = true;
1725 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1726 if (!KisResourceCacheDb::addTags(storage, resourceType)) {
1727 qWarning() << "Failed to add all tags for storage" << storage;
1728 r = false;
1729 }
1730 }
1731 return r;
1732}
1733
1735{
1736 // location is already relative
1737
1738 try {
1739 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
1740
1741 {
1742 KisSqlQueryLoader loader(":/sql/delete_versioned_resources_for_storage_indirect.sql",
1744 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1745 loader.exec();
1746 }
1747
1748 {
1749 KisSqlQueryLoader loader(":/sql/delete_resource_tags_for_storage_indirect.sql",
1751 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1752 loader.exec();
1753 }
1754
1755 {
1756 KisSqlQueryLoader loader(":/sql/delete_versioned_resources_for_storage_direct.sql",
1758 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1759 loader.exec();
1760 if (loader.query().numRowsAffected() > 0) {
1761 qWarning() << "WARNING: deleteStorage: versioned_resurces table contained resource versions not being "
1762 "present in the main table. Deleted: "
1763 << loader.query().numRowsAffected();
1764 }
1765 }
1766
1767 {
1768 KisSqlQueryLoader loader(":/sql/delete_resource_metadata_for_storage.sql",
1770 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1771 loader.query().bindValue(":table", METADATA_RESOURCES);
1772 loader.exec();
1773 }
1774
1775 {
1776 KisSqlQueryLoader loader("inline://delete_current_resources_for_storage",
1777 "DELETE FROM resources\n"
1778 "WHERE storage_id = (SELECT storages.id\n"
1779 " FROM storages\n"
1780 " WHERE storages.location = :location)\n",
1782 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1783 loader.exec();
1784 }
1785
1798 QVariantList uniqueTagIdsToDelete;
1799 {
1800 auto [unique, shared] = tagsForStorage(ResourceType::PaintOpPresets, location);
1801 std::copy(unique.begin(), unique.end(), std::back_inserter(uniqueTagIdsToDelete));
1802 }
1803
1804 {
1805 KisSqlQueryLoader loader("inline://delete_tags_storage_links_for_storage",
1806 "WITH storage_id_query AS (\n"
1807 " SELECT storages.id\n"
1808 " FROM storages\n"
1809 " WHERE storages.location = :location)\n"
1810 "DELETE FROM tags_storages\n"
1811 "WHERE storage_id IN storage_id_query\n",
1813 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1814 loader.exec();
1815 }
1816
1817 if (!uniqueTagIdsToDelete.isEmpty()) {
1818 {
1819 KisSqlQueryLoader loader("inline://delete_tags_translations_for_storage",
1820 "DELETE FROM tag_translations WHERE tag_id = ?",
1822 loader.query().addBindValue(uniqueTagIdsToDelete);
1823 loader.execBatch();
1824 }
1825
1826 {
1827 KisSqlQueryLoader loader("inline://delete_resource_tags_for_storage",
1828 "DELETE FROM resource_tags WHERE tag_id = ?",
1830 loader.query().addBindValue(uniqueTagIdsToDelete);
1831 loader.execBatch();
1832 }
1833
1834 {
1835 KisSqlQueryLoader loader("inline://delete_tags_for_storage",
1836 "DELETE FROM tags WHERE id = ?",
1838 loader.query().addBindValue(uniqueTagIdsToDelete);
1839 loader.execBatch();
1840 }
1841 }
1842
1843 {
1844 KisSqlQueryLoader loader("inline://delete_starage_metadata_for_storage",
1845 "DELETE FROM metadata\n"
1846 "WHERE foreign_id = (SELECT storages.id\n"
1847 " FROM storages\n"
1848 " WHERE storages.location = :location)"
1849 "AND table_name = :table;",
1851 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1852 loader.query().bindValue(":table", METADATA_STORAGES);
1853 loader.exec();
1854 }
1855
1856 {
1857 KisSqlQueryLoader loader("inline://delete_storage",
1858 "DELETE FROM storages\n"
1859 "WHERE location = :location;",
1861 loader.query().bindValue(":location", changeToEmptyIfNull(location));
1862 loader.exec();
1863 }
1864
1865 transactionLock.commit();
1866
1867 } catch (const KisSqlQueryLoader::FileException &e) {
1868 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
1869 qWarning().noquote() << " file:" << e.filePath;
1870 qWarning().noquote() << " error:" << e.fileErrorString;
1871 return false;
1872 } catch (const KisSqlQueryLoader::SQLException &e) {
1873 qWarning().noquote() << "ERROR: deleteStorage:" << e.message;
1874 qWarning().noquote() << " file:" << e.filePath;
1875 qWarning().noquote() << " statement:" << e.statementIndex;
1876 qWarning().noquote() << " error:" << e.sqlError.text();
1877 return false;
1878 }
1879
1880 return true;
1881}
1882
1884{
1885 return deleteStorage(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location()));
1886}
1887
1888namespace {
1889struct ResourceVersion : public boost::less_than_comparable<ResourceVersion>
1890{
1891 int resourceId = -1;
1892 int version = -1;
1893 QDateTime timestamp;
1894 QString url;
1895
1896 bool operator<(const ResourceVersion &rhs) const {
1897 return resourceId < rhs.resourceId ||
1898 (resourceId == rhs.resourceId && version < rhs.version);
1899 }
1900
1901 struct CompareByResourceId {
1902 bool operator() (const ResourceVersion &lhs, const ResourceVersion &rhs) const {
1903 return lhs.resourceId < rhs.resourceId;
1904 }
1905 };
1906
1907
1908};
1909
1910[[maybe_unused]]
1911QDebug operator<<(QDebug dbg, const ResourceVersion &ver)
1912{
1913 dbg.nospace() << "ResourceVersion("
1914 << ver.resourceId << ", "
1915 << ver.version << ", "
1916 << ver.timestamp << ", "
1917 << ver.url << ")";
1918
1919 return dbg.space();
1920}
1921}
1922
1924{
1925 QElapsedTimer t;
1926 t.start();
1927
1928 if (!s_valid) {
1929 qWarning() << "KisResourceCacheDb::addResource: The database is not valid";
1930 return false;
1931 }
1932
1933 bool success = true;
1934
1935 // Find the storage in the database
1936 QSqlQuery q;
1937 if (!q.prepare("SELECT id\n"
1938 ", timestamp\n"
1939 ", pre_installed\n"
1940 "FROM storages\n"
1941 "WHERE location = :location\n")) {
1942 qWarning() << "Could not prepare storage timestamp statement" << q.lastError();
1943 }
1944
1945 q.bindValue(":location", changeToEmptyIfNull(KisResourceLocator::instance()->makeStorageLocationRelative(storage->location())));
1946 if (!q.exec()) {
1947 qWarning() << "Could not execute storage timestamp statement" << q.boundValues() << q.lastError();
1948 }
1949
1950 if (!q.first()) {
1951 // This is a new storage, the user must have dropped it in the path before restarting Krita, so add it.
1952 debugResource << "Adding storage to the database:" << storage;
1953 if (!addStorage(storage, false)) {
1954 qWarning() << "Could not add new storage" << storage->name() << "to the database";
1955 success = false;
1956 }
1957 return success;
1958 }
1959
1960 storage->setStorageId(q.value("id").toInt());
1961
1963 QSqlDatabase::database().transaction();
1964
1967
1968 Q_FOREACH(const QString &resourceType, KisResourceLoaderRegistry::instance()->resourceTypes()) {
1969
1972
1973 QVector<ResourceVersion> resourcesInStorage;
1974
1978
1979 int nextInexistentResourceId = std::numeric_limits<int>::min();
1980
1981 QSharedPointer<KisResourceStorage::ResourceIterator> iter = storage->resources(resourceType);
1982 while (iter->hasNext()) {
1983 iter->next();
1984
1985 const int firstResourceVersionPosition = resourcesInStorage.size();
1986
1987 int detectedResourceId = nextInexistentResourceId;
1989 iter->versions();
1990
1991 while (verIt->hasNext()) {
1992 verIt->next();
1993
1994 // verIt->url() contains paths like "brushes/ink.png" or "brushes/subfolder/splash.png".
1995 // we need to cut off the first part and get "ink.png" in the first case,
1996 // but "subfolder/splash.png" in the second case in order for subfolders to work
1997 // so it cannot just use QFileInfo(verIt->url()).fileName() here.
1998 QString path = QDir::fromNativeSeparators(verIt->url()); // make sure it uses Unix separators
1999 int folderEndIdx = path.indexOf("/");
2000 QString properFilenameWithSubfolders = path.right(path.length() - folderEndIdx - 1);
2001 int id = resourceIdForResource(properFilenameWithSubfolders,
2002 verIt->type(),
2004
2005 ResourceVersion item;
2006 item.url = verIt->url();
2007 item.version = verIt->guessedVersion();
2008
2009 // we use lower precision than the normal QDateTime
2010 item.timestamp = QDateTime::fromSecsSinceEpoch(verIt->lastModified().toSecsSinceEpoch());
2011
2012 item.resourceId = id;
2013
2014 if (detectedResourceId < 0 && id >= 0) {
2015 detectedResourceId = id;
2016 }
2017
2018 resourcesInStorage.append(item);
2019 }
2020
2024
2025 for (int i = firstResourceVersionPosition; i < resourcesInStorage.size(); i++) {
2026 if (resourcesInStorage[i].resourceId < 0) {
2027 resourcesInStorage[i].resourceId = detectedResourceId;
2028 }
2029 }
2030
2031 nextInexistentResourceId++;
2032 }
2033
2034
2036
2037 QVector<ResourceVersion> resourcesInDatabase;
2038
2039 QSqlQuery q;
2040 q.setForwardOnly(true);
2041 if (!q.prepare("SELECT versioned_resources.resource_id, versioned_resources.filename, versioned_resources.version, versioned_resources.timestamp\n"
2042 "FROM versioned_resources\n"
2043 ", resource_types\n"
2044 ", resources\n"
2045 "WHERE resources.resource_type_id = resource_types.id\n"
2046 "AND resources.id = versioned_resources.resource_id\n"
2047 "AND resource_types.name = :resource_type\n"
2048 "AND versioned_resources.storage_id == :storage_id")) {
2049 qWarning() << "Could not prepare resource by type query" << q.lastError();
2050 success = false;
2051 continue;
2052 }
2053
2054 q.bindValue(":resource_type", resourceType);
2055 q.bindValue(":storage_id", int(storage->storageId()));
2056
2057 if (!q.exec()) {
2058 qWarning() << "Could not exec resource by type query" << q.boundValues() << q.lastError();
2059 success = false;
2060 continue;
2061 }
2062
2063 while (q.next()) {
2064 ResourceVersion item;
2065 item.url = resourceType + "/" + q.value(1).toString();
2066 item.version = q.value(2).toInt();
2067 item.timestamp = QDateTime::fromSecsSinceEpoch(q.value(3).toInt());
2068 item.resourceId = q.value(0).toInt();
2069
2070 resourcesInDatabase.append(item);
2071 }
2072
2073 QSet<int> resourceIdForUpdate;
2074
2075 std::sort(resourcesInStorage.begin(), resourcesInStorage.end());
2076 std::sort(resourcesInDatabase.begin(), resourcesInDatabase.end());
2077
2078 auto itA = resourcesInStorage.begin();
2079 auto endA = resourcesInStorage.end();
2080
2081 auto itB = resourcesInDatabase.begin();
2082 auto endB = resourcesInDatabase.end();
2083
2087
2088 while (itA != endA) {
2089 if (itA->resourceId >= 0) break;
2090
2091 KoResourceSP res = storage->resource(itA->url);
2092
2093 if (!res) {
2094 KisUsageLogger::log("Could not load resource " + itA->url);
2095 ++itA;
2096 continue;
2097 }
2098
2099 res->setVersion(itA->version);
2100 res->setMD5Sum(storage->resourceMd5(itA->url));
2101 if (!res->valid()) {
2102 KisUsageLogger::log("Could not retrieve md5 for resource " + itA->url);
2103 ++itA;
2104 continue;
2105 }
2106
2107 const bool retval = addResource(storage, itA->timestamp, res, resourceType);
2108 if (!retval) {
2109 KisUsageLogger::log("Could not add resource " + itA->url);
2110 ++itA;
2111 continue;
2112 }
2113
2114 const int resourceId = res->resourceId();
2115 KIS_SAFE_ASSERT_RECOVER(resourceId >= 0) {
2116 KisUsageLogger::log("Could not get id for resource " + itA->url);
2117 ++itA;
2118 continue;
2119 }
2120
2121 auto nextResource = std::upper_bound(itA, endA, *itA, ResourceVersion::CompareByResourceId());
2122 for (auto it = std::next(itA); it != nextResource; ++it) {
2123 KoResourceSP res = storage->resource(it->url);
2124 res->setVersion(it->version);
2125 res->setMD5Sum(storage->resourceMd5(it->url));
2126 if (!res->valid()) {
2127 continue;
2128 }
2129
2130 const bool retval = addResourceVersion(resourceId, it->timestamp, storage, res);
2131 KIS_SAFE_ASSERT_RECOVER(retval) {
2132 KisUsageLogger::log("Could not add version for resource " + itA->url);
2133 continue;
2134 }
2135 }
2136
2137 itA = nextResource;
2138 }
2139
2147
2148 while (itA != endA || itB != endB) {
2149 if ((itA != endA && itB != endB && *itA < *itB) ||
2150 itB == endB) {
2151
2152 // add a version to the database
2153
2154 KoResourceSP res = storage->resource(itA->url);
2155 if (res) {
2156 res->setVersion(itA->version);
2157 res->setMD5Sum(storage->resourceMd5(itA->url));
2158
2159 const bool result = addResourceVersionImpl(itA->resourceId, itA->timestamp, storage, res);
2161
2162 resourceIdForUpdate.insert(itA->resourceId);
2163 }
2164 ++itA;
2165
2166 } else if ((itA != endA && itB != endB && *itA > *itB) ||
2167 itA == endA) {
2168
2169 // remove a version from the database
2170 const bool result = removeResourceVersionImpl(itB->resourceId, itB->version, storage);
2172 resourceIdForUpdate.insert(itB->resourceId);
2173 ++itB;
2174
2175 } else {
2176 // resources are equal, just skip them
2177 ++itA;
2178 ++itB;
2179 }
2180 }
2181
2182
2186
2187 for (auto it = resourceIdForUpdate.begin(); it != resourceIdForUpdate.end(); ++it) {
2188 updateResourceTableForResourceIfNeeded(*it, resourceType, storage);
2189 }
2190 }
2191
2192 QSqlDatabase::database().commit();
2193 debugResource << "Synchronizing the storages took" << t.elapsed() << "milliseconds for" << storage->location();
2194
2195 return success;
2196}
2197
2199{
2200 try {
2201 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
2202
2206 {
2207 KisSqlQueryLoader loader(
2208 "inline://delete_metadata_for_resources_in_memory_storages",
2209 "DELETE FROM metadata\n"
2210 "WHERE foreign_id IN (SELECT id\n"
2211 " FROM resources\n"
2212 " WHERE storage_id in (SELECT id\n"
2213 " FROM storages\n"
2214 " WHERE storage_type_id == :storage_type))\n"
2215 "AND table_name = :table",
2217 loader.query().bindValue(":table", METADATA_RESOURCES);
2218 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2219 loader.exec();
2220 }
2221
2222 {
2223 KisSqlQueryLoader loader("inline://delete_metadata_for_temporary_resources",
2224 "DELETE FROM metadata\n"
2225 "WHERE foreign_id IN (SELECT id\n"
2226 " FROM resources\n"
2227 " WHERE temporary = 1)\n"
2228 "AND table_name = :table",
2230 loader.query().bindValue(":table", METADATA_RESOURCES);
2231 loader.exec();
2232 }
2233
2234 {
2235 KisSqlQueryLoader loader("inline://delete_versions_of_resources_in_temporary_storages",
2236 "DELETE FROM versioned_resources\n"
2237 "WHERE storage_id in (SELECT id\n"
2238 " FROM storages\n"
2239 " WHERE storage_type_id == :storage_type)",
2241 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2242 loader.exec();
2243 }
2244
2245 {
2246 KisSqlQueryLoader loader("inline://delete_versions_of_temporary_resources",
2247 "DELETE FROM versioned_resources\n"
2248 "WHERE resource_id IN (SELECT id FROM resources\n"
2249 " WHERE temporary = 1)",
2251 loader.exec();
2252 }
2253
2254 {
2255 KisSqlQueryLoader loader("inline://delete_current_resources_in_temporary_storages",
2256 "DELETE FROM resources\n"
2257 "WHERE storage_id in (SELECT id\n"
2258 " FROM storages\n"
2259 " WHERE storage_type_id == :storage_type)",
2261 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2262 loader.exec();
2263 }
2264
2265 {
2266 KisSqlQueryLoader loader("inline://delete_current_temporary_resources",
2267 "DELETE FROM resources\n"
2268 "WHERE temporary = 1",
2270 loader.exec();
2271 }
2272
2277 {
2278 KisSqlQueryLoader loader("inline://delete_metadata_for_temporary_storages",
2279 "DELETE FROM metadata\n"
2280 "WHERE foreign_id IN (SELECT id\n"
2281 " FROM storages\n"
2282 " WHERE storage_type_id == :storage_type)\n"
2283 "AND table_name = :table;",
2285 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2286 loader.query().bindValue(":table", METADATA_STORAGES);
2287 loader.exec();
2288 }
2289
2290 {
2291 KisSqlQueryLoader loader("inline://delete_temporary_storages",
2292 "DELETE FROM storages\n"
2293 "WHERE storage_type_id == :storage_type\n",
2295 loader.query().bindValue(":storage_type", (int)KisResourceStorage::StorageType::Memory);
2296 loader.exec();
2297 }
2298
2299 transactionLock.commit();
2300 } catch (const KisSqlQueryLoader::SQLException &e) {
2301 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2302 qWarning().noquote() << " file:" << e.filePath;
2303 qWarning().noquote() << " statement:" << e.statementIndex;
2304 qWarning().noquote() << " error:" << e.sqlError.text();
2305 }
2306}
2307
2309{
2310 QSqlQuery q;
2311
2312 if (!q.prepare("PRAGMA optimize;")) {
2313 qWarning() << "Could not prepare query" << q.lastQuery() << q.lastError();
2314 }
2315
2316 if (!q.exec()) {
2317 qWarning() << "Could not execute query" << q.lastQuery() << q.lastError();
2318 }
2319}
2320
2322{
2323 KisSqlQueryLoader loader("inline://set_foreign_keys_state",
2324 QString("PRAGMA foreign_keys = %1").arg(isEnabled ? "ON" : "OFF"));
2325 loader.exec();
2326}
2327
2329{
2330 KisSqlQueryLoader loader("inline://get_foreign_keys_state",
2331 "PRAGMA foreign_keys");
2332
2333 loader.exec();
2334
2335 if (loader.query().first()) {
2336 return loader.query().value(0).toInt();
2337 }
2338
2339 return false;
2340}
2341
2343{
2344#ifdef KRITA_STABLE
2345 bool useForeignKeys = false;
2346 KisUsageLogger::log("INFO: detected stable build of Krita, foreign_keys constraint will be disabled");
2347#else
2348 bool useForeignKeys = true;
2349 KisUsageLogger::log("INFO: detected unstable build of Krita, foreign_keys constraint will be enabled");
2350#endif
2351
2352 if (qEnvironmentVariableIsSet("KRITA_OVERRIDE_USE_FOREIGN_KEYS")) {
2353 useForeignKeys = qEnvironmentVariableIntValue("KRITA_OVERRIDE_USE_FOREIGN_KEYS") > 0;
2354 KisUsageLogger::log("INFO: foreign_keys constraint was overridden by KRITA_OVERRIDE_USE_FOREIGN_KEYS: " + QString::number(useForeignKeys));
2355 }
2356
2357 try {
2358 const bool oldForeignKeysState = KisResourceCacheDb::getForeignKeysStateImpl();
2359
2360 if (oldForeignKeysState != useForeignKeys) {
2362 "INFO: switch foreign_keys state: " +
2363 QString::number(oldForeignKeysState) +
2364 " -> " +
2365 QString::number(useForeignKeys));
2366
2368 }
2369
2370 } catch (const KisSqlQueryLoader::SQLException &e) {
2371 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2372 qWarning().noquote() << " file:" << e.filePath;
2373 qWarning().noquote() << " statement:" << e.statementIndex;
2374 qWarning().noquote() << " error:" << e.sqlError.text();
2375 }
2376
2377}
2378
2379bool KisResourceCacheDb::registerResourceType(const QString &resourceType)
2380{
2381 // Check whether the type already exists
2382 {
2383 QSqlQuery q;
2384 if (!q.prepare("SELECT count(*)\n"
2385 "FROM resource_types\n"
2386 "WHERE name = :resource_type\n")) {
2387 qWarning() << "Could not prepare select from resource_types query" << q.lastError();
2388 return false;
2389 }
2390 q.bindValue(":resource_type", resourceType);
2391 if (!q.exec()) {
2392 qWarning() << "Could not execute select from resource_types query" << q.lastError();
2393 return false;
2394 }
2395 q.first();
2396 int rowCount = q.value(0).toInt();
2397 if (rowCount > 0) {
2398 return true;
2399 }
2400 }
2401 // if not, add it
2402 QFile f(":/fill_resource_types.sql");
2403 if (f.open(QFile::ReadOnly)) {
2404 QString sql = f.readAll();
2405 QSqlQuery q(sql);
2406 q.addBindValue(resourceType);
2407 if (!q.exec()) {
2408 qWarning() << "Could not insert" << resourceType << q.lastError();
2409 return false;
2410 }
2411 return true;
2412 }
2413 qWarning() << "Could not open fill_resource_types.sql";
2414 return false;
2415}
2416
2417QMap<QString, QVariant> KisResourceCacheDb::metaDataForId(int id, const QString &tableName)
2418{
2419 QMap<QString, QVariant> map;
2420
2421 QSqlQuery q;
2422 q.setForwardOnly(true);
2423 if (!q.prepare("SELECT key\n"
2424 ", value\n"
2425 "FROM metadata\n"
2426 "WHERE foreign_id = :id\n"
2427 "AND table_name = :table")) {
2428 qWarning() << "Could not prepare metadata query" << q.lastError();
2429 return map;
2430 }
2431
2432 q.bindValue(":id", id);
2433 q.bindValue(":table", tableName);
2434
2435 if (!q.exec()) {
2436 qWarning() << "Could not execute metadata query" << q.lastError();
2437 return map;
2438 }
2439
2440 while (q.next()) {
2441 QString key = q.value(0).toString();
2442 QByteArray ba = q.value(1).toByteArray();
2443 if (!ba.isEmpty()) {
2444 QDataStream ds(QByteArray::fromBase64(ba));
2445 QVariant value;
2446 ds.setVersion(QDataStream::Qt_5_15); // so Qt6 can read metatypes written by Qt5
2447 ds >> value;
2448 map[key] = value;
2449 }
2450 }
2451
2452 return map;
2453}
2454
2455bool KisResourceCacheDb::updateMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2456{
2457 QSqlDatabase::database().transaction();
2458
2459 {
2460 QSqlQuery q;
2461 if (!q.prepare("DELETE FROM metadata\n"
2462 "WHERE foreign_id = :id\n"
2463 "AND table_name = :table\n")) {
2464 QSqlDatabase::database().rollback();
2465 qWarning() << "Could not prepare delete metadata query" << q.lastError();
2466 return false;
2467 }
2468
2469 q.bindValue(":id", id);
2470 q.bindValue(":table", tableName);
2471
2472 if (!q.exec()) {
2473 QSqlDatabase::database().rollback();
2474 qWarning() << "Could not execute delete metadata query" << q.lastError();
2475 return false;
2476
2477 }
2478 }
2479
2480 if (addMetaDataForId(map, id, tableName)) {
2481 QSqlDatabase::database().commit();
2482 }
2483 else {
2484 QSqlDatabase::database().rollback();
2485 }
2486 return true;
2487}
2488
2489bool KisResourceCacheDb::addMetaDataForId(const QMap<QString, QVariant> map, int id, const QString &tableName)
2490{
2491
2492 QSqlQuery q;
2493 if (!q.prepare("INSERT INTO metadata\n"
2494 "(foreign_id, table_name, key, value)\n"
2495 "VALUES\n"
2496 "(:id, :table, :key, :value)")) {
2497 QSqlDatabase::database().rollback();
2498 qWarning() << "Could not create insert metadata query" << q.lastError();
2499 return false;
2500 }
2501
2502 QMap<QString, QVariant>::const_iterator iter = map.cbegin();
2503 while (iter != map.cend()) {
2504 q.bindValue(":id", id);
2505 q.bindValue(":table", tableName);
2506 q.bindValue(":key", iter.key());
2507
2508 QVariant v = iter.value();
2509 if (!v.isNull() && v.isValid()) {
2510 QByteArray ba;
2511 QDataStream ds(&ba, QIODevice::WriteOnly);
2512 ds.setVersion(QDataStream::Qt_5_15); // so Qt6 can write metatypes readable by Qt5
2513 ds << v;
2514 ba = ba.toBase64();
2515 q.bindValue(":value", QString::fromLatin1(ba));
2516
2517 if (!q.exec()) {
2518 qWarning() << "Could not insert metadata" << q.lastError();
2519 return false;
2520 }
2521 }
2522 ++iter;
2523 }
2524 return true;
2525}
2526
2528{
2529 auto deleteMetadataForType = [] (const QString &tableName) {
2530 KisSqlQueryLoader loader("inline://delete_orphaned_records (" + tableName + ")",
2531 QString("DELETE FROM metadata\n"
2532 "WHERE foreign_id NOT IN (SELECT id FROM %1)\n"
2533 "AND table_name = \"%1\"\n")
2534 .arg(tableName));
2535 loader.exec();
2536
2537 if (loader.query().numRowsAffected() > 0) {
2538 qWarning().noquote().nospace() << "WARNING: orphaned metadata records were found for " << tableName << "!";
2539 qWarning().noquote().nospace() << " Num records removed: " << loader.query().numRowsAffected();
2540 }
2541 };
2542
2543 try {
2544 KisDatabaseTransactionLock transactionLock(QSqlDatabase::database());
2545
2546 deleteMetadataForType(METADATA_RESOURCES);
2547 deleteMetadataForType(METADATA_STORAGES);
2548
2549 transactionLock.commit();
2550
2551 } catch (const KisSqlQueryLoader::SQLException &e) {
2552 qWarning().noquote() << "ERROR: failed to execute query:" << e.message;
2553 qWarning().noquote() << " file:" << e.filePath;
2554 qWarning().noquote() << " statement:" << e.statementIndex;
2555 qWarning().noquote() << " error:" << e.sqlError.text();
2556
2557 return false;
2558 }
2559
2560 return true;
2561}
float value(const T *src, size_t ch)
qreal v
QDebug KRITACOMMAND_EXPORT operator<<(QDebug dbg, const KisCumulativeUndoData &data)
QList< QString > QStringList
QString changeToEmptyIfNull(QString s)
const QString dbDriver
bool updateSchemaVersion()
QSqlError runUpdateScriptFile(const QString &path, const QString &message)
QSqlError runUpdateScript(const QString &script, const QString &message)
const QString METADATA_RESOURCES
QSqlError createDatabase(const QString &location)
const QString METADATA_STORAGES
bool operator<(KoSnapStrategy::SnapType lhs, KoSnapStrategy::SnapType rhs)
#define debugResource
static bool numberedBackupFile(const QString &filename, const QString &backupDir=QString(), const QString &backupExtension=QStringLiteral("~"), const uint maxBackups=10)
Definition KisBackup.cpp:38
static QStringList disabledBundles
the list of compatibility bundles that need to inactive by default
static bool addStorage(KisResourceStorageSP storage, bool preinstalled)
static bool addResource(KisResourceStorageSP storage, QDateTime timestamp, KoResourceSP resource, const QString &resourceType)
static void setForeignKeysStateImpl(bool isEnabled)
static bool isValid()
isValid
static bool resourceNeedsUpdating(int resourceId, QDateTime timestamp)
static bool getAllVersionsLocations(int resourceId, QStringList &outVersionsLocationsList)
static bool addResourceVersionImpl(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
static bool getForeignKeysStateImpl()
static bool addResourceVersion(int resourceId, QDateTime timestamp, KisResourceStorageSP storage, KoResourceSP resource)
addResourceVersion adds a new version of the resource to the database. The resource itself already sh...
static void performHouseKeepingOnExit()
perform optimize and vacuum when necessary
static bool removeOrphanedMetaData()
removeOrphanedMetaData Previous versions of Krita never removed metadata, so this function doublechec...
static int resourceIdForResource(const QString &resourceFileName, const QString &resourceType, const QString &storageLocation)
static bool updateMetaDataForId(const QMap< QString, QVariant > map, int id, const QString &tableName)
setMetaDataForId removes all metadata for the given id and table name, and inserts the metadata in th...
static void deleteTemporaryResources()
Delete all storages that are Unknown or Memory and all resources that are marked temporary or belong ...
static QMap< QString, QVariant > metaDataForId(int id, const QString &tableName)
metaDataForId
static bool initialize(const QString &location)
initializes the database and updates the scheme if necessary. Does not actually fill the database wit...
static bool removeResourceVersionImpl(int resourceId, int version, KisResourceStorageSP storage)
static bool addTag(const QString &resourceType, const QString storageLocation, KisTagSP tag)
static QString lastError()
lastError returns the last SQL error.
static QVector< int > resourcesForStorage(const QString &resourceType, const QString &storageLocation)
static bool getResourceIdFromFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
The function will find the resource only if it is the latest version.
static const QString resourceCacheDbFilename
filename of the database
static bool linkTagToStorage(const QString &url, const QString &resourceType, const QString &storageLocation)
static bool deleteStorage(KisResourceStorageSP storage)
Actually delete the storage and all its resources from the database (i.e., nothing is set to inactive...
static bool setResourceActive(int resourceId, bool active=false)
Make this resource active or inactive; this does not remove the resource from disk or from the databa...
static bool addMetaDataForId(const QMap< QString, QVariant > map, int id, const QString &tableName)
static QStringList storageTypes
kinds of places where resources can be stored
static bool addResources(KisResourceStorageSP storage, QString resourceType)
static bool removeResourceCompletely(int resourceId)
static bool addStorageTags(KisResourceStorageSP storage)
static std::pair< QVector< int >, QVector< int > > tagsForStorage(const QString &resourceType, const QString &storageLocation)
static QString s_lastError
static bool getResourceIdFromVersionedFilename(QString filename, QString resourceType, QString storageLocation, int &outResourceId)
Note that here you can put even the original filename - any filename from the versioned_resources - a...
static const QString databaseVersion
current schema version
static void synchronizeForeignKeysState()
static bool registerResourceType(const QString &resourceType)
registerResourceType registers this resource type in the database
static bool makeResourceTheCurrentVersion(int resourceId, KoResourceSP resource)
static bool synchronizeStorage(KisResourceStorageSP storage)
static bool registerStorageType(const KisResourceStorage::StorageType storageType)
registerStorageType registers this storage type in the database
static bool updateResourceTableForResourceIfNeeded(int resourceId, const QString &resourceType, KisResourceStorageSP storage)
static bool hasTag(const QString &url, const QString &resourceType)
static bool addTags(KisResourceStorageSP storage, QString resourceType)
static bool tagResource(const QString &resourceFileName, KisTagSP tag, const QString &resourceType)
static KisResourceLoaderRegistry * instance()
QString makeStorageLocationRelative(QString location) const
static void saveTags()
saveTags saves all tags to .tag files in the resource folder
static KisResourceLocator * instance()
static QString storageTypeToUntranslatedString(StorageType storageType)
static constexpr single_statement_mode_t single_statement_mode
static void log(const QString &message)
Logs with date/time.
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define warnDbMigration
Definition kis_debug.h:86
#define infoDbMigration
Definition kis_debug.h:66
KRITAVERSION_EXPORT QString versionString(bool checkGit=false)
const QString PaintOpPresets