Make sure DB upgrades happen atomically

This commit is contained in:
Luis Ángel San Martín
2025-06-15 18:12:07 +02:00
parent c621c2705b
commit 6a4f9730f5

View File

@ -901,224 +901,229 @@ bool DataBaseManagement::updateToCurrentVersion(const QString &libraryPath)
{ {
QSqlDatabase db = loadDatabaseFromFile(libraryDatabasePath); QSqlDatabase db = loadDatabaseFromFile(libraryDatabasePath);
if (db.isValid() && db.isOpen()) { if (db.isValid() && db.isOpen()) {
if (pre7) // TODO: execute only if previous version was < 7.0 if (!db.transaction()) {
{ QLOG_ERROR() << "Failed to start transaction for database update";
// new 7.0 fields returnValue = false;
QStringList columnDefs; } else {
columnDefs << "hasBeenOpened BOOLEAN DEFAULT 0" if (pre7) {
<< "rating INTEGER DEFAULT 0" // new 7.0 fields
<< "currentPage INTEGER DEFAULT 1"
<< "bookmark1 INTEGER DEFAULT -1"
<< "bookmark2 INTEGER DEFAULT -1"
<< "bookmark3 INTEGER DEFAULT -1"
<< "brightness INTEGER DEFAULT -1"
<< "contrast INTEGER DEFAULT -1"
<< "gamma INTEGER DEFAULT -1";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
// TODO update hasBeenOpened value
if (pre7_1) {
{
QStringList columnDefs; QStringList columnDefs;
columnDefs << "finished BOOLEAN DEFAULT 0" columnDefs << "hasBeenOpened BOOLEAN DEFAULT 0"
<< "completed BOOLEAN DEFAULT 1"; << "rating INTEGER DEFAULT 0"
bool successAddingColumns = addColumns("folder", columnDefs, db); << "currentPage INTEGER DEFAULT 1"
returnValue = returnValue && successAddingColumns; << "bookmark1 INTEGER DEFAULT -1"
} << "bookmark2 INTEGER DEFAULT -1"
<< "bookmark3 INTEGER DEFAULT -1"
<< "brightness INTEGER DEFAULT -1"
<< "contrast INTEGER DEFAULT -1"
<< "gamma INTEGER DEFAULT -1";
{ // comic_info
QStringList columnDefs;
columnDefs << "comicVineID TEXT DEFAULT NULL";
bool successAddingColumns = addColumns("comic_info", columnDefs, db); bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns; returnValue = returnValue && successAddingColumns;
} }
}
if (pre8) { if (pre7_1) {
bool successCreatingNewTables = createV8Tables(db); {
returnValue = returnValue && successCreatingNewTables; QStringList columnDefs;
} columnDefs << "finished BOOLEAN DEFAULT 0"
<< "completed BOOLEAN DEFAULT 1";
if (pre9_5) { bool successAddingColumns = addColumns("folder", columnDefs, db);
{ // folder returnValue = returnValue && successAddingColumns;
QStringList columnDefs;
// a full library update is needed after updating the table
columnDefs << "numChildren INTEGER";
columnDefs << "firstChildHash TEXT";
columnDefs << "customImage TEXT";
bool successAddingColumns = addColumns("folder", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
{ // comic_info
QStringList columnDefs;
columnDefs << "lastTimeOpened INTEGER";
columnDefs << "coverSizeRatio REAL";
columnDefs << "originalCoverSize TEXT";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
QSqlQuery queryIndexLastTimeOpened(db);
bool successCreatingIndex = queryIndexLastTimeOpened.exec("CREATE INDEX last_time_opened_index ON comic_info (lastTimeOpened)");
returnValue = returnValue && successCreatingIndex;
}
// update folders info
{
DBHelper::updateChildrenInfo(db);
}
{
QSqlQuery selectQuery(db);
selectQuery.prepare("SELECT id, hash FROM comic_info");
selectQuery.exec();
db.transaction();
QSqlQuery updateCoverInfo(db);
updateCoverInfo.prepare("UPDATE comic_info SET coverSizeRatio = :coverSizeRatio WHERE id = :id");
QImageReader thumbnail;
while (selectQuery.next()) {
auto coverPath = LibraryPaths::coverPath(libraryPath, selectQuery.value(1).toString());
thumbnail.setFileName(coverPath);
float coverSizeRatio = static_cast<float>(thumbnail.size().width()) / thumbnail.size().height();
updateCoverInfo.bindValue(":coverSizeRatio", coverSizeRatio);
updateCoverInfo.bindValue(":id", selectQuery.value(0));
updateCoverInfo.exec();
} }
db.commit(); { // comic_info
QStringList columnDefs;
columnDefs << "comicVineID TEXT DEFAULT NULL";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
} }
}
if (pre9_8) { if (pre8) {
{ // comic_info bool successCreatingNewTables = createV8Tables(db);
QStringList columnDefs; returnValue = returnValue && successCreatingNewTables;
columnDefs << "manga BOOLEAN DEFAULT 0";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
} }
{ // folder
QStringList columnDefs;
columnDefs << "manga BOOLEAN DEFAULT 0";
bool successAddingColumns = addColumns("folder", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
}
if (pre9_13) { if (pre9_5) {
{ // comic_info { // folder
QStringList columnDefs; QStringList columnDefs;
columnDefs << "added INTEGER"; // a full library update is needed after updating the table
columnDefs << "type INTEGER DEFAULT 0"; // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic, columnDefs << "numChildren INTEGER";
columnDefs << "editor TEXT"; columnDefs << "firstChildHash TEXT";
columnDefs << "imprint TEXT"; columnDefs << "customImage TEXT";
columnDefs << "teams TEXT"; bool successAddingColumns = addColumns("folder", columnDefs, db);
columnDefs << "locations TEXT"; returnValue = returnValue && successAddingColumns;
columnDefs << "series TEXT"; }
columnDefs << "alternateSeries TEXT";
columnDefs << "alternateNumber TEXT";
columnDefs << "alternateCount INTEGER";
columnDefs << "languageISO TEXT";
columnDefs << "seriesGroup TEXT";
columnDefs << "mainCharacterOrTeam TEXT";
columnDefs << "review TEXT";
columnDefs << "tags TEXT";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
QSqlQuery updateTypeQueryToManga(db); { // comic_info
updateTypeQueryToManga.prepare("UPDATE comic_info SET type = manga"); QStringList columnDefs;
bool successMigratingManga = updateTypeQueryToManga.exec(); columnDefs << "lastTimeOpened INTEGER";
returnValue = returnValue && successMigratingManga; columnDefs << "coverSizeRatio REAL";
columnDefs << "originalCoverSize TEXT";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
QSqlQuery updateNumberQueryToBis(db); QSqlQuery queryIndexLastTimeOpened(db);
updateNumberQueryToBis.prepare("UPDATE comic_info SET number = number + 0.5 WHERE isBis = 1"); bool successCreatingIndex = queryIndexLastTimeOpened.exec("CREATE INDEX last_time_opened_index ON comic_info (lastTimeOpened)");
bool successMigratingBis = updateNumberQueryToBis.exec(); returnValue = returnValue && successCreatingIndex;
returnValue = returnValue && successMigratingBis; }
}
{ // folder
QStringList columnDefs;
columnDefs << "added INTEGER";
columnDefs << "updated INTEGER";
columnDefs << "type INTEGER DEFAULT 0";
bool successAddingColumns = addColumns("folder", columnDefs, db); // update folders info
returnValue = returnValue && successAddingColumns; {
DBHelper::updateChildrenInfo(db);
}
QSqlQuery updateTypeQueryToManga(db); {
updateTypeQueryToManga.prepare("UPDATE folder SET type = manga"); QSqlQuery selectQuery(db);
bool successMigratingManga = updateTypeQueryToManga.exec(); selectQuery.prepare("SELECT id, hash FROM comic_info");
returnValue = returnValue && successMigratingManga; selectQuery.exec();
}
}
// ensure that INTEGER types migrated to TEXT are actually changed in the table definition to avoid internal type castings, this happened in 9.13 but a migration wasn't shipped with that version. db.transaction();
if (pre9_14) {
{
bool pre9_14_successfulMigration = true;
QSqlQuery pragmaFKOFF(db); QSqlQuery updateCoverInfo(db);
pragmaFKOFF.prepare("PRAGMA foreign_keys=OFF"); updateCoverInfo.prepare("UPDATE comic_info SET coverSizeRatio = :coverSizeRatio WHERE id = :id");
pre9_14_successfulMigration = pre9_14_successfulMigration && pragmaFKOFF.exec();
db.transaction(); QImageReader thumbnail;
while (selectQuery.next()) {
auto coverPath = LibraryPaths::coverPath(libraryPath, selectQuery.value(1).toString());
thumbnail.setFileName(coverPath);
pre9_14_successfulMigration = pre9_14_successfulMigration && createComicInfoTable(db, "comic_info_migration"); float coverSizeRatio = static_cast<float>(thumbnail.size().width()) / thumbnail.size().height();
updateCoverInfo.bindValue(":coverSizeRatio", coverSizeRatio);
updateCoverInfo.bindValue(":id", selectQuery.value(0));
QSqlQuery copyComicInfoToComicInfoMigration(db); updateCoverInfo.exec();
copyComicInfoToComicInfoMigration.prepare("INSERT INTO comic_info_migration SELECT * FROM comic_info"); }
pre9_14_successfulMigration = pre9_14_successfulMigration && copyComicInfoToComicInfoMigration.exec();
QSqlQuery dropComicInfo(db);
dropComicInfo.prepare("DROP TABLE comic_info");
pre9_14_successfulMigration = pre9_14_successfulMigration && dropComicInfo.exec();
QSqlQuery renameComicInfoMigrationToComicInfo(db);
renameComicInfoMigrationToComicInfo.prepare("ALTER TABLE comic_info_migration RENAME TO comic_info");
pre9_14_successfulMigration = pre9_14_successfulMigration && renameComicInfoMigrationToComicInfo.exec();
if (pre9_14_successfulMigration)
db.commit(); db.commit();
else }
db.rollback();
QSqlQuery pragmaFKON1("PRAGMA foreign_keys=ON", db);
returnValue = returnValue && pre9_14_successfulMigration;
} }
}
if (pre9_16) { if (pre9_8) {
{ // comic_info { // comic_info
QStringList columnDefs; QStringList columnDefs;
columnDefs << "imageFiltersJson TEXT"; columnDefs << "manga BOOLEAN DEFAULT 0";
columnDefs << "lastTimeImageFiltersSet INTEGER DEFAULT 0"; bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
{ // folder
QStringList columnDefs;
columnDefs << "manga BOOLEAN DEFAULT 0";
bool successAddingColumns = addColumns("folder", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
}
columnDefs << "lastTimeCoverSet INTEGER DEFAULT 0"; if (pre9_13) {
columnDefs << "usesExternalCover BOOLEAN DEFAULT 0"; { // comic_info
QStringList columnDefs;
columnDefs << "added INTEGER";
columnDefs << "type INTEGER DEFAULT 0"; // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic,
columnDefs << "editor TEXT";
columnDefs << "imprint TEXT";
columnDefs << "teams TEXT";
columnDefs << "locations TEXT";
columnDefs << "series TEXT";
columnDefs << "alternateSeries TEXT";
columnDefs << "alternateNumber TEXT";
columnDefs << "alternateCount INTEGER";
columnDefs << "languageISO TEXT";
columnDefs << "seriesGroup TEXT";
columnDefs << "mainCharacterOrTeam TEXT";
columnDefs << "review TEXT";
columnDefs << "tags TEXT";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
columnDefs << "lastTimeMetadataSet INTEGER DEFAULT 0"; QSqlQuery updateTypeQueryToManga(db);
updateTypeQueryToManga.prepare("UPDATE comic_info SET type = manga");
bool successMigratingManga = updateTypeQueryToManga.exec();
returnValue = returnValue && successMigratingManga;
bool successAddingColumns = addColumns("comic_info", columnDefs, db); QSqlQuery updateNumberQueryToBis(db);
returnValue = returnValue && successAddingColumns; updateNumberQueryToBis.prepare("UPDATE comic_info SET number = number + 0.5 WHERE isBis = 1");
bool successMigratingBis = updateNumberQueryToBis.exec();
returnValue = returnValue && successMigratingBis;
}
{ // folder
QStringList columnDefs;
columnDefs << "added INTEGER";
columnDefs << "updated INTEGER";
columnDefs << "type INTEGER DEFAULT 0";
bool successAddingColumns = addColumns("folder", columnDefs, db);
returnValue = returnValue && successAddingColumns;
QSqlQuery updateTypeQueryToManga(db);
updateTypeQueryToManga.prepare("UPDATE folder SET type = manga");
bool successMigratingManga = updateTypeQueryToManga.exec();
returnValue = returnValue && successMigratingManga;
}
}
// ensure that INTEGER types migrated to TEXT are actually changed in the table definition to avoid internal type castings, this happened in 9.13 but a migration wasn't shipped with that version.
if (pre9_14) {
{
bool pre9_14_successfulMigration = true;
QSqlQuery pragmaFKOFF(db);
pragmaFKOFF.prepare("PRAGMA foreign_keys=OFF");
pre9_14_successfulMigration = pre9_14_successfulMigration && pragmaFKOFF.exec();
pre9_14_successfulMigration = pre9_14_successfulMigration && createComicInfoTable(db, "comic_info_migration");
QSqlQuery copyComicInfoToComicInfoMigration(db);
copyComicInfoToComicInfoMigration.prepare("INSERT INTO comic_info_migration SELECT * FROM comic_info");
pre9_14_successfulMigration = pre9_14_successfulMigration && copyComicInfoToComicInfoMigration.exec();
QSqlQuery dropComicInfo(db);
dropComicInfo.prepare("DROP TABLE comic_info");
pre9_14_successfulMigration = pre9_14_successfulMigration && dropComicInfo.exec();
QSqlQuery renameComicInfoMigrationToComicInfo(db);
renameComicInfoMigrationToComicInfo.prepare("ALTER TABLE comic_info_migration RENAME TO comic_info");
pre9_14_successfulMigration = pre9_14_successfulMigration && renameComicInfoMigrationToComicInfo.exec();
QSqlQuery pragmaFKON1("PRAGMA foreign_keys=ON", db);
returnValue = returnValue && pre9_14_successfulMigration;
}
}
if (pre9_16) {
{ // comic_info
QStringList columnDefs;
columnDefs << "imageFiltersJson TEXT";
columnDefs << "lastTimeImageFiltersSet INTEGER DEFAULT 0";
columnDefs << "lastTimeCoverSet INTEGER DEFAULT 0";
columnDefs << "usesExternalCover BOOLEAN DEFAULT 0";
columnDefs << "lastTimeMetadataSet INTEGER DEFAULT 0";
bool successAddingColumns = addColumns("comic_info", columnDefs, db);
returnValue = returnValue && successAddingColumns;
}
}
if (returnValue) {
QSqlQuery updateVersion(db);
updateVersion.prepare("UPDATE db_info SET "
"version = :version");
updateVersion.bindValue(":version", DB_VERSION);
updateVersion.exec();
returnValue = updateVersion.numRowsAffected() > 0;
} }
} }
if (returnValue) { if (returnValue) {
QSqlQuery updateVersion(db); if (!db.commit()) {
updateVersion.prepare("UPDATE db_info SET " QLOG_ERROR() << "Failed to commit transaction for database update";
"version = :version"); returnValue = false;
updateVersion.bindValue(":version", DB_VERSION); }
updateVersion.exec(); } else {
db.rollback();
returnValue = updateVersion.numRowsAffected() > 0;
} }
} }
connectionName = db.connectionName(); connectionName = db.connectionName();