File indexing completed on 2024-04-21 03:42:38
0001 /* 0002 SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include <limits> 0008 #include <cmath> 0009 #include <QSqlDriver> 0010 #include <QSqlRecord> 0011 #include <QMutexLocker> 0012 #include <QTemporaryDir> 0013 #include <qsqldatabase.h> 0014 #include "cachingdms.h" 0015 #include "catalogsdb.h" 0016 #include "kspaths.h" 0017 #include "skymesh.h" 0018 #include "Options.h" 0019 #include "final_action.h" 0020 #include "sqlstatements.cpp" 0021 0022 using namespace CatalogsDB; 0023 0024 /** 0025 * Get an increasing index for new connections. 0026 */ 0027 int get_connection_index() 0028 { 0029 static int connection_index = 0; 0030 return connection_index++; 0031 } 0032 0033 QSqlQuery make_query(QSqlDatabase &db, const QString &statement, const bool forward_only) 0034 { 0035 QSqlQuery query{ db }; 0036 0037 query.setForwardOnly(forward_only); 0038 if (!query.prepare(statement)) 0039 { 0040 throw DatabaseError("Can't prepare query!", DatabaseError::ErrorType::PREPARE, 0041 query.lastError()); 0042 }; 0043 0044 return query; 0045 } 0046 0047 /** 0048 * Migrate the database from \p version to the current version. 0049 */ 0050 std::pair<bool, QString> migrate_db(const int version, QSqlDatabase &db, 0051 QString prefix = "") 0052 { 0053 if (prefix.size() > 0) 0054 prefix += "."; 0055 0056 // we have to add the timestamp collumn to the catalogs 0057 if (version < 2) 0058 { 0059 QSqlQuery add_ts{ db }; 0060 const auto success = add_ts.exec(QString("ALTER TABLE %1catalogs ADD COLUMN " 0061 "timestamp DEFAULT NULL") 0062 .arg(prefix)); 0063 if (!success) 0064 return { false, add_ts.lastError().text() }; 0065 } 0066 0067 // adding the color selector table; this only applies for the 0068 // master database 0069 if (version < 3 && prefix == "") 0070 { 0071 QSqlQuery add_colors{ db }; 0072 const auto success = add_colors.exec(SqlStatements::create_colors_table); 0073 if (!success) 0074 return { false, add_colors.lastError().text() }; 0075 } 0076 0077 return { true, "" }; 0078 } 0079 0080 DBManager::DBManager(const QString &filename) : m_db_file(filename) 0081 { 0082 m_db = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); 0083 m_db.setDatabaseName(m_db_file); 0084 0085 // we are throwing here, because errors at this stage should be fatal 0086 if (!m_db.open()) 0087 { 0088 throw DatabaseError(QString("Cannot open CatalogDatabase '%1'!").arg(m_db_file), 0089 DatabaseError::ErrorType::OPEN, m_db.lastError()); 0090 } 0091 0092 bool init = false; 0093 std::tie(m_db_version, m_htmesh_level, init) = get_db_meta(); 0094 0095 if (!init && m_db_version > 0 && m_db_version < SqlStatements::current_db_version) 0096 { 0097 const auto &backup_path = QString("%1.%2").arg(m_db_file).arg( 0098 QDateTime::currentDateTime().toString("dd_MMMM_yy_hh_mm_sss_zzz")); 0099 0100 if (!QFile::copy(m_db_file, backup_path)) 0101 { 0102 throw DatabaseError( 0103 QString("Could not backup dso database before upgrading."), 0104 DatabaseError::ErrorType::VERSION, QSqlError{}); 0105 } 0106 0107 const auto &success = migrate_db(m_db_version, m_db); 0108 if (success.first) 0109 { 0110 m_db_version = SqlStatements::current_db_version; 0111 QSqlQuery version_query{ m_db }; 0112 version_query.prepare(SqlStatements::update_version); 0113 version_query.bindValue(":version", m_db_version); 0114 0115 if (!version_query.exec()) 0116 { 0117 throw DatabaseError(QString("Could not update the database version."), 0118 DatabaseError::ErrorType::VERSION, 0119 version_query.lastError()); 0120 } 0121 } 0122 else 0123 throw DatabaseError( 0124 QString("Wrong database version. Expected %1 and got %2 and " 0125 "migration is not possible.") 0126 .arg(SqlStatements::current_db_version) 0127 .arg(m_db_version), 0128 DatabaseError::ErrorType::VERSION, success.second); 0129 } 0130 0131 QSqlQuery master_exists{ m_db }; 0132 master_exists.exec(SqlStatements::exists_master); 0133 const bool master_does_exist = master_exists.next(); 0134 master_exists.finish(); 0135 0136 if (init || !master_does_exist) 0137 { 0138 if (!initialize_db()) 0139 { 0140 throw DatabaseError(QString("Could not initialize database."), 0141 DatabaseError::ErrorType::INIT, m_db.lastError()); 0142 } 0143 0144 if (!catalog_exists(SqlStatements::user_catalog_id)) 0145 { 0146 const auto &res = 0147 register_catalog(SqlStatements::user_catalog_id, 0148 SqlStatements::user_catalog_name, true, true, 1); 0149 if (!res.first) 0150 { 0151 throw DatabaseError(QString("Could not create user database."), 0152 DatabaseError::ErrorType::CREATE_CATALOG, res.second); 0153 } 0154 } 0155 0156 if (!update_catalog_views()) 0157 { 0158 throw DatabaseError(QString("Unable to create combined catalog view!"), 0159 DatabaseError::ErrorType::CREATE_CATALOG, 0160 m_db.lastError()); 0161 } 0162 0163 if (!compile_master_catalog()) 0164 { 0165 throw DatabaseError(QString("Unable to create master catalog!"), 0166 DatabaseError::ErrorType::CREATE_MASTER, 0167 m_db.lastError()); 0168 } 0169 } 0170 0171 m_q_cat_by_id = make_query(m_db, SqlStatements::get_catalog_by_id, true); 0172 m_q_obj_by_trixel = make_query(m_db, SqlStatements::dso_by_trixel, false); 0173 m_q_obj_by_trixel_no_nulls = make_query(m_db, SqlStatements::dso_by_trixel_no_nulls, false); 0174 m_q_obj_by_trixel_null_mag = make_query(m_db, SqlStatements::dso_by_trixel_null_mag, false); 0175 m_q_obj_by_name = make_query(m_db, SqlStatements::dso_by_name, true); 0176 m_q_obj_by_name_exact = make_query(m_db, SqlStatements::dso_by_name_exact, true); 0177 m_q_obj_by_lim = make_query(m_db, SqlStatements::dso_by_lim, true); 0178 m_q_obj_by_maglim = make_query(m_db, SqlStatements::dso_by_maglim, true); 0179 m_q_obj_by_maglim_and_type = 0180 make_query(m_db, SqlStatements::dso_by_maglim_and_type, true); 0181 m_q_obj_by_oid = make_query(m_db, SqlStatements::dso_by_oid, true); 0182 }; 0183 0184 DBManager::DBManager(const DBManager &other) : DBManager::DBManager{ other.m_db_file } {}; 0185 0186 bool DBManager::initialize_db() 0187 { 0188 if (m_db_version < 0 || m_htmesh_level < 1) 0189 throw std::runtime_error("DBManager not initialized properly, m_db_vesion and " 0190 "m_htmesh_level have to be set."); 0191 0192 if (!m_db.exec(SqlStatements::create_meta_table).isActive()) 0193 return false; 0194 0195 if (!m_db.exec(SqlStatements::create_colors_table).isActive()) 0196 return false; 0197 0198 QSqlQuery meta_query{ m_db }; 0199 meta_query.prepare(SqlStatements::set_meta); 0200 meta_query.bindValue(0, m_db_version); 0201 meta_query.bindValue(1, m_htmesh_level); 0202 meta_query.bindValue(2, false); 0203 0204 if (!meta_query.exec()) 0205 return false; 0206 0207 return m_db.exec(SqlStatements::create_catalog_list_table).isActive(); 0208 } 0209 0210 std::tuple<int, int, bool> DBManager::get_db_meta() 0211 { 0212 auto query = m_db.exec(SqlStatements::get_meta); 0213 0214 if (query.first()) 0215 return { query.value(0).toInt(), query.value(1).toInt(), 0216 query.value(2).toBool() }; 0217 else 0218 return { SqlStatements::current_db_version, SqlStatements::default_htmesh_level, 0219 true }; 0220 } 0221 0222 std::vector<int> DBManager::get_catalog_ids(bool include_disabled) 0223 { 0224 auto query = m_db.exec(include_disabled ? SqlStatements::get_all_catalog_ids : 0225 SqlStatements::get_catalog_ids); 0226 0227 std::vector<int> ids; 0228 0229 while (query.next()) 0230 { 0231 int id = query.value(0).toInt(); 0232 ids.push_back(id); 0233 } 0234 0235 return ids; 0236 } 0237 0238 bool DBManager::update_catalog_views() 0239 { 0240 const auto &ids = get_catalog_ids(); 0241 bool result = true; 0242 auto _ = gsl::finally([&]() 0243 { 0244 m_db.commit(); 0245 }); 0246 0247 m_db.transaction(); 0248 QSqlQuery query{ m_db }; 0249 result &= 0250 query.exec(QString("DROP VIEW IF EXISTS ") + SqlStatements::all_catalog_view); 0251 0252 if (!result) 0253 { 0254 return result; 0255 } 0256 0257 QString view 0258 { 0259 "CREATE VIEW " 0260 }; // small enough to be included here and not in sqlstatements 0261 0262 view += SqlStatements::all_catalog_view; 0263 view += " AS\n"; 0264 0265 QStringList prefixed{}; 0266 for (auto *field : SqlStatements::catalog_collumns) 0267 { 0268 prefixed << QString("c.") + field; 0269 } 0270 0271 QString prefixed_joined = prefixed.join(","); 0272 0273 QStringList catalog_queries{}; 0274 for (auto id : ids) 0275 { 0276 catalog_queries << SqlStatements::all_catalog_view_body( 0277 prefixed_joined, SqlStatements::catalog_prefix, id); 0278 } 0279 0280 if (ids.size() == 0) 0281 { 0282 catalog_queries << SqlStatements::all_catalog_view_body( 0283 prefixed_joined, SqlStatements::catalog_prefix, 0) + 0284 " WHERE FALSE"; // we blackhole the query 0285 } 0286 0287 view += catalog_queries.join("\nUNION ALL\n"); 0288 result &= query.exec(view); 0289 return result; 0290 } 0291 0292 void bind_catalog(QSqlQuery &query, const Catalog &cat) 0293 { 0294 query.bindValue(":id", cat.id); 0295 query.bindValue(":name", cat.name); 0296 query.bindValue(":mut", cat.mut); 0297 query.bindValue(":enabled", cat.enabled); 0298 query.bindValue(":precedence", cat.precedence); 0299 query.bindValue(":author", cat.author); 0300 query.bindValue(":source", cat.source); 0301 query.bindValue(":description", cat.description); 0302 query.bindValue(":version", cat.version); 0303 query.bindValue(":color", cat.color); 0304 query.bindValue(":license", cat.license); 0305 query.bindValue(":maintainer", cat.maintainer); 0306 query.bindValue(":timestamp", cat.timestamp); 0307 } 0308 0309 std::pair<bool, QString> DBManager::register_catalog( 0310 const int id, const QString &name, const bool mut, const bool enabled, 0311 const double precedence, const QString &author, const QString &source, 0312 const QString &description, const int version, const QString &color, 0313 const QString &license, const QString &maintainer, const QDateTime ×tamp) 0314 { 0315 return register_catalog({ id, name, precedence, author, source, description, mut, 0316 enabled, version, color, license, maintainer, timestamp }); 0317 } 0318 0319 std::pair<bool, QString> DBManager::register_catalog(const Catalog &cat) 0320 { 0321 if (catalog_exists(cat.id)) 0322 return { false, i18n("Catalog with that ID already exists.") }; 0323 0324 QSqlQuery query{ m_db }; 0325 0326 if (!query.exec(SqlStatements::create_catalog_table(cat.id))) 0327 { 0328 return { false, query.lastError().text() }; 0329 } 0330 0331 query.prepare(SqlStatements::insert_catalog); 0332 bind_catalog(query, cat); 0333 0334 return { query.exec(), query.lastError().text() }; 0335 }; 0336 0337 bool DBManager::compile_master_catalog() 0338 { 0339 auto _ = gsl::finally([&]() 0340 { 0341 m_db.commit(); 0342 }); 0343 QSqlQuery query{ m_db }; 0344 m_db.transaction(); 0345 0346 if (!query.exec(SqlStatements::drop_master)) 0347 { 0348 return false; 0349 } 0350 0351 if (!query.exec(SqlStatements::create_master)) 0352 { 0353 return false; 0354 } 0355 0356 bool success = true; 0357 success &= query.exec(SqlStatements::create_master_trixel_index); 0358 success &= query.exec(SqlStatements::create_master_mag_index); 0359 success &= query.exec(SqlStatements::create_master_type_index); 0360 success &= query.exec(SqlStatements::create_master_name_index); 0361 return success; 0362 }; 0363 0364 const Catalog read_catalog(const QSqlQuery &query) 0365 { 0366 return { query.value("id").toInt(), 0367 query.value("name").toString(), 0368 query.value("precedence").toDouble(), 0369 query.value("author").toString(), 0370 query.value("source").toString(), 0371 query.value("description").toString(), 0372 query.value("mut").toBool(), 0373 query.value("enabled").toBool(), 0374 query.value("version").toInt(), 0375 query.value("color").toString(), 0376 query.value("license").toString(), 0377 query.value("maintainer").toString(), 0378 query.value("timestamp").toDateTime() }; 0379 } 0380 0381 const std::pair<bool, Catalog> DBManager::get_catalog(const int id) 0382 { 0383 QMutexLocker _{ &m_mutex }; 0384 m_q_cat_by_id.bindValue(0, id); 0385 0386 if (!m_q_cat_by_id.exec()) 0387 return { false, {} }; 0388 0389 if (!m_q_cat_by_id.next()) 0390 return { false, {} }; 0391 0392 Catalog cat{ read_catalog(m_q_cat_by_id) }; 0393 0394 m_q_cat_by_id.finish(); 0395 return { true, cat }; 0396 } 0397 0398 bool DBManager::catalog_exists(const int id) 0399 { 0400 QMutexLocker _{ &m_mutex }; 0401 m_q_cat_by_id.bindValue(0, id); 0402 auto end = gsl::finally([&]() 0403 { 0404 m_q_cat_by_id.finish(); 0405 }); 0406 0407 if (!m_q_cat_by_id.exec()) 0408 return false; 0409 0410 return m_q_cat_by_id.next(); 0411 } 0412 0413 size_t count_rows(QSqlQuery &query) 0414 { 0415 size_t count{ 0 }; 0416 while (query.next()) 0417 { 0418 count++; 0419 } 0420 0421 return count; 0422 } 0423 0424 CatalogObject DBManager::read_catalogobject(const QSqlQuery &query) const 0425 { 0426 const CatalogObject::oid id = query.value(0).toByteArray(); 0427 const SkyObject::TYPE type = static_cast<SkyObject::TYPE>(query.value(1).toInt()); 0428 0429 const double ra = query.value(2).toDouble(); 0430 const double dec = query.value(3).toDouble(); 0431 const float mag = query.isNull(4) ? NaN::f : query.value(4).toFloat(); 0432 const QString name = query.value(5).toString(); 0433 const QString long_name = query.value(6).toString(); 0434 const QString catalog_identifier = query.value(7).toString(); 0435 const float major = query.value(8).toFloat(); 0436 const float minor = query.value(9).toFloat(); 0437 const double position_angle = query.value(10).toDouble(); 0438 const float flux = query.value(11).toFloat(); 0439 const int catalog_id = query.value(12).toInt(); 0440 0441 return { id, type, dms(ra), dms(dec), 0442 mag, name, long_name, catalog_identifier, 0443 catalog_id, major, minor, position_angle, 0444 flux, m_db_file }; 0445 } 0446 0447 CatalogObjectVector DBManager::_get_objects_in_trixel_generic(QSqlQuery &query, const int trixel) 0448 { 0449 QMutexLocker _{ &m_mutex }; // this costs ~ .1ms which is ok 0450 query.bindValue(0, trixel); 0451 0452 if (!query.exec()) // we throw because this is not recoverable 0453 throw DatabaseError( 0454 QString("The by-trixel query for objects in trixel=%1 failed.") 0455 .arg(trixel), 0456 DatabaseError::ErrorType::UNKNOWN, query.lastError()); 0457 0458 CatalogObjectVector objects; 0459 size_t count = 0460 count_rows(query); // this also moves the query head to the end 0461 0462 if (count == 0) 0463 { 0464 query.finish(); 0465 return objects; 0466 } 0467 0468 objects.reserve(count); 0469 0470 while (query.previous()) 0471 { 0472 objects.push_back(read_catalogobject(query)); 0473 } 0474 0475 query.finish(); 0476 0477 // move semantics baby! 0478 return objects; 0479 } 0480 0481 CatalogObjectList DBManager::fetch_objects(QSqlQuery &query) const 0482 { 0483 CatalogObjectList objects; 0484 auto _ = gsl::finally([&]() 0485 { 0486 query.finish(); 0487 }); 0488 0489 query.exec(); 0490 0491 if (!query.isActive()) 0492 return {}; 0493 while (query.next()) 0494 objects.push_back(read_catalogobject(query)); 0495 0496 return objects; 0497 } 0498 0499 CatalogObjectList DBManager::find_objects_by_name(const QString &name, const int limit, 0500 const bool exactMatchOnly) 0501 { 0502 QMutexLocker _{ &m_mutex }; 0503 0504 // limit < 0 is a sentinel value for unlimited 0505 if (limit == 0) 0506 return CatalogObjectList(); 0507 0508 // search for an exact match first 0509 m_q_obj_by_name_exact.bindValue(":name", name); 0510 CatalogObjectList objs { fetch_objects(m_q_obj_by_name_exact) }; 0511 0512 if ((limit == 1 && objs.size() > 0) || exactMatchOnly) 0513 return objs; 0514 0515 Q_ASSERT(objs.size() <= 1); 0516 0517 m_q_obj_by_name.bindValue(":name", name); 0518 m_q_obj_by_name.bindValue(":limit", int(limit - objs.size())); 0519 0520 CatalogObjectList moreObjects = fetch_objects(m_q_obj_by_name); 0521 moreObjects.splice(moreObjects.begin(), objs); 0522 return moreObjects; 0523 0524 } 0525 0526 CatalogObjectList DBManager::find_objects_by_name(const int catalog_id, 0527 const QString &name, const int limit) 0528 { 0529 QSqlQuery query{ m_db }; 0530 0531 query.prepare(SqlStatements::dso_by_name_and_catalog(catalog_id)); 0532 query.bindValue(":name", name); 0533 query.bindValue(":limit", limit); 0534 query.bindValue(":catalog", catalog_id); 0535 0536 return fetch_objects(query); 0537 } 0538 0539 std::pair<bool, CatalogObject> DBManager::read_first_object(QSqlQuery &query) const 0540 { 0541 if (!query.exec() || !query.first()) 0542 return { false, {} }; 0543 0544 return { true, read_catalogobject(query) }; 0545 } 0546 0547 std::pair<bool, CatalogObject> DBManager::get_object(const CatalogObject::oid &oid) 0548 { 0549 QMutexLocker _{ &m_mutex }; 0550 m_q_obj_by_oid.bindValue(0, oid); 0551 0552 auto f = gsl::finally([&]() // taken from the GSL, runs when it goes out of scope 0553 { 0554 m_q_obj_by_oid.finish(); 0555 }); 0556 0557 return read_first_object(m_q_obj_by_oid); 0558 }; 0559 0560 std::pair<bool, CatalogObject> DBManager::get_object(const CatalogObject::oid &oid, 0561 const int catalog_id) 0562 { 0563 QMutexLocker _{ &m_mutex }; 0564 QSqlQuery query{ m_db }; 0565 0566 query.prepare(SqlStatements::dso_by_oid_and_catalog(catalog_id)); 0567 query.bindValue(0, oid); 0568 0569 return read_first_object(query); 0570 }; 0571 0572 CatalogObjectList DBManager::get_objects(float maglim, int limit) 0573 { 0574 QMutexLocker _{ &m_mutex }; 0575 m_q_obj_by_maglim.bindValue(":maglim", maglim); 0576 m_q_obj_by_maglim.bindValue(":limit", limit); 0577 0578 return fetch_objects(m_q_obj_by_maglim); 0579 } 0580 0581 CatalogObjectList DBManager::get_objects_all() 0582 { 0583 QMutexLocker _{ &m_mutex }; 0584 m_q_obj_by_lim.bindValue(":limit", -1); 0585 0586 return fetch_objects(m_q_obj_by_lim); 0587 } 0588 0589 CatalogObjectList DBManager::get_objects(SkyObject::TYPE type, float maglim, int limit) 0590 { 0591 QMutexLocker _{ &m_mutex }; 0592 m_q_obj_by_maglim_and_type.bindValue(":type", type); 0593 m_q_obj_by_maglim_and_type.bindValue(":limit", limit); 0594 m_q_obj_by_maglim_and_type.bindValue(":maglim", maglim); 0595 0596 return fetch_objects(m_q_obj_by_maglim_and_type); 0597 } 0598 0599 CatalogObjectList DBManager::get_objects_in_catalog(SkyObject::TYPE type, 0600 const int catalog_id, float maglim, 0601 int limit) 0602 { 0603 QSqlQuery query{ m_db }; 0604 0605 query.prepare(SqlStatements::dso_in_catalog_by_maglim(catalog_id)); 0606 query.bindValue(":type", type); 0607 query.bindValue(":limit", limit); 0608 query.bindValue(":maglim", maglim); 0609 return fetch_objects(query); 0610 } 0611 0612 std::pair<bool, QString> DBManager::set_catalog_enabled(const int id, const bool enabled) 0613 { 0614 const auto &success = get_catalog(id); 0615 if (!success.first) 0616 return { false, i18n("Catalog could not be found.") }; 0617 0618 const auto &cat = success.second; 0619 if (cat.enabled == enabled) 0620 return { true, "" }; 0621 0622 QSqlQuery query{ m_db }; 0623 query.prepare(SqlStatements::enable_disable_catalog); 0624 query.bindValue(":enabled", enabled); 0625 query.bindValue(":id", id); 0626 0627 return { query.exec() &&update_catalog_views() &&compile_master_catalog(), 0628 query.lastError().text() + m_db.lastError().text() }; 0629 } 0630 0631 const std::vector<Catalog> DBManager::get_catalogs(bool include_disabled) 0632 { 0633 auto ids = get_catalog_ids(include_disabled); 0634 std::vector<Catalog> catalogs; 0635 catalogs.reserve(ids.size()); 0636 0637 std::transform(ids.cbegin(), ids.cend(), std::back_inserter(catalogs), 0638 [&](const int id) 0639 { 0640 const auto &found = get_catalog(id); 0641 if (found.first) 0642 return found.second; 0643 0644 // This really should **not** happen 0645 throw DatabaseError( 0646 QString("Could not retrieve the catalog with id=%1").arg(id)); 0647 }); 0648 0649 return catalogs; 0650 } 0651 0652 inline void bind_catalogobject(QSqlQuery &query, const int catalog_id, 0653 const SkyObject::TYPE t, const CachingDms &r, 0654 const CachingDms &d, const QString &n, const float m, 0655 const QString &lname, const QString &catalog_identifier, 0656 const float a, const float b, const double pa, 0657 const float flux, Trixel trixel, 0658 const CatalogObject::oid &new_id) 0659 { 0660 query.prepare(SqlStatements::insert_dso(catalog_id)); 0661 0662 query.bindValue(":hash", new_id); // no dedupe, maybe in the future 0663 query.bindValue(":oid", new_id); 0664 query.bindValue(":type", static_cast<int>(t)); 0665 query.bindValue(":ra", r.Degrees()); 0666 query.bindValue(":dec", d.Degrees()); 0667 query.bindValue(":magnitude", (m < 99 && !std::isnan(m)) ? m : QVariant{}); 0668 query.bindValue(":name", n); 0669 query.bindValue(":long_name", lname.length() > 0 ? lname : QVariant{}); 0670 query.bindValue(":catalog_identifier", 0671 catalog_identifier.length() > 0 ? catalog_identifier : QVariant{}); 0672 query.bindValue(":major_axis", a > 0 ? a : QVariant{}); 0673 query.bindValue(":minor_axis", b > 0 ? b : QVariant{}); 0674 query.bindValue(":position_angle", pa > 0 ? pa : QVariant{}); 0675 query.bindValue(":flux", flux > 0 ? flux : QVariant{}); 0676 query.bindValue(":trixel", trixel); 0677 query.bindValue(":catalog", catalog_id); 0678 } 0679 0680 inline void bind_catalogobject(QSqlQuery &query, const int catalog_id, 0681 const CatalogObject &obj, Trixel trixel) 0682 { 0683 bind_catalogobject(query, catalog_id, static_cast<SkyObject::TYPE>(obj.type()), 0684 obj.ra0(), obj.dec0(), obj.name(), obj.mag(), obj.longname(), 0685 obj.catalogIdentifier(), obj.a(), obj.b(), obj.pa(), obj.flux(), 0686 trixel, obj.getObjectId()); 0687 }; 0688 0689 std::pair<bool, QString> DBManager::add_object(const int catalog_id, 0690 const CatalogObject &obj) 0691 { 0692 return add_object(catalog_id, static_cast<SkyObject::TYPE>(obj.type()), obj.ra0(), 0693 obj.dec0(), obj.name(), obj.mag(), obj.longname(), 0694 obj.catalogIdentifier(), obj.a(), obj.b(), obj.pa(), obj.flux()); 0695 } 0696 0697 std::pair<bool, QString> 0698 DBManager::add_object(const int catalog_id, const SkyObject::TYPE t, const CachingDms &r, 0699 const CachingDms &d, const QString &n, const float m, 0700 const QString &lname, const QString &catalog_identifier, 0701 const float a, const float b, const double pa, const float flux) 0702 { 0703 { 0704 const auto &success = get_catalog(catalog_id); 0705 if (!success.first) 0706 return { false, i18n("Catalog with id=%1 not found.", catalog_id) }; 0707 0708 if (!success.second.mut) 0709 return { false, i18n("Catalog is immutable!") }; 0710 } 0711 0712 SkyPoint tmp{ r, d }; 0713 const auto trixel = SkyMesh::Create(m_htmesh_level)->index(&tmp); 0714 QSqlQuery query{ m_db }; 0715 0716 const auto new_id = 0717 CatalogObject::getId(t, r.Degrees(), d.Degrees(), n, catalog_identifier); 0718 bind_catalogobject(query, catalog_id, t, r, d, n, m, lname, catalog_identifier, a, b, 0719 pa, flux, trixel, new_id); 0720 0721 if (!query.exec()) 0722 { 0723 auto err = query.lastError().text(); 0724 if (err.startsWith("UNIQUE")) 0725 err = i18n("The object is already in the catalog!"); 0726 0727 return { false, i18n("Could not insert object! %1", err) }; 0728 } 0729 0730 return { update_catalog_views() &&compile_master_catalog(), 0731 m_db.lastError().text() }; 0732 } 0733 0734 std::pair<bool, QString> DBManager::remove_object(const int catalog_id, 0735 const CatalogObject::oid &id) 0736 { 0737 QSqlQuery query{ m_db }; 0738 0739 query.prepare(SqlStatements::remove_dso(catalog_id)); 0740 query.bindValue(":oid", id); 0741 0742 if (!query.exec()) 0743 return { false, query.lastError().text() }; 0744 0745 return { update_catalog_views() &&compile_master_catalog(), 0746 m_db.lastError().text() }; 0747 } 0748 0749 std::pair<bool, QString> DBManager::dump_catalog(int catalog_id, QString file_path) 0750 { 0751 const auto &found = get_catalog(catalog_id); 0752 if (!found.first) 0753 return { false, i18n("Catalog could not be found.") }; 0754 0755 QFile file{ file_path }; 0756 if (!file.open(QIODevice::WriteOnly)) 0757 return { false, i18n("Output file is not writable.") }; 0758 file.resize(0); 0759 file.close(); 0760 0761 QSqlQuery query{ m_db }; 0762 0763 if (!query.exec(QString("ATTACH [%1] AS tmp").arg(file_path))) 0764 return { false, 0765 i18n("Could not attach output file.<br>%1", query.lastError().text()) }; 0766 0767 m_db.transaction(); 0768 auto _ = gsl::finally([&]() // taken from the GSL, runs when it goes out of scope 0769 { 0770 m_db.commit(); 0771 query.exec("DETACH tmp"); 0772 }); 0773 0774 if (!query.exec( 0775 QString("CREATE TABLE tmp.cat AS SELECT * FROM cat_%1").arg(catalog_id))) 0776 return { false, i18n("Could not copy catalog to output file.<br>%1") 0777 .arg(query.lastError().text()) }; 0778 0779 if (!query.exec(SqlStatements::create_catalog_registry("tmp.catalogs"))) 0780 return { false, i18n("Could not create catalog registry in output file.<br>%1") 0781 .arg(query.lastError().text()) }; 0782 0783 query.prepare(SqlStatements::insert_into_catalog_registry("tmp.catalogs")); 0784 0785 auto cat = found.second; 0786 cat.enabled = true; 0787 bind_catalog(query, cat); 0788 0789 if (!query.exec()) 0790 { 0791 return { false, 0792 i18n("Could not insert catalog into registry in output file.<br>%1") 0793 .arg(query.lastError().text()) }; 0794 } 0795 0796 if (!query.exec(QString("PRAGMA tmp.user_version = %1").arg(m_db_version))) 0797 { 0798 return { false, i18n("Could not insert set exported database version.<br>%1") 0799 .arg(query.lastError().text()) }; 0800 } 0801 0802 if (!query.exec(QString("PRAGMA tmp.application_id = %1").arg(application_id))) 0803 { 0804 return { false, 0805 i18n("Could not insert set exported database application id.<br>%1") 0806 .arg(query.lastError().text()) }; 0807 } 0808 0809 return { true, {} }; 0810 } 0811 0812 std::pair<bool, QString> DBManager::import_catalog(const QString &file_path, 0813 const bool overwrite) 0814 { 0815 QTemporaryDir tmp; 0816 const auto new_path = tmp.filePath("cat.kscat"); 0817 QFile::copy(file_path, new_path); 0818 0819 QFile file{ new_path }; 0820 if (!file.open(QIODevice::ReadOnly)) 0821 return { false, i18n("Catalog file is not readable.") }; 0822 file.close(); 0823 0824 QSqlQuery query{ m_db }; 0825 0826 if (!query.exec(QString("ATTACH [%1] AS tmp").arg(new_path))) 0827 { 0828 m_db.commit(); 0829 return { false, 0830 i18n("Could not attach input file.<br>%1", query.lastError().text()) }; 0831 } 0832 0833 auto _ = gsl::finally([&]() 0834 { 0835 m_db.commit(); 0836 query.exec("DETACH tmp"); 0837 }); 0838 0839 if (!query.exec("PRAGMA tmp.application_id") || !query.next() || 0840 query.value(0).toInt() != CatalogsDB::application_id) 0841 return { false, i18n("Invalid catalog file.") }; 0842 0843 if (!query.exec("PRAGMA tmp.user_version") || !query.next() || 0844 query.value(0).toInt() < m_db_version) 0845 { 0846 const auto &success = migrate_db(query.value(0).toInt(), m_db, "tmp"); 0847 if (!success.first) 0848 return { false, i18n("Could not migrate old catalog format.<br>%1", 0849 success.second) }; 0850 } 0851 0852 if (!query.exec("SELECT id FROM tmp.catalogs LIMIT 1") || !query.next()) 0853 return { false, 0854 i18n("Could read the catalog id.<br>%1", query.lastError().text()) }; 0855 0856 const auto id = query.value(0).toInt(); 0857 query.finish(); 0858 0859 { 0860 const auto &found = get_catalog(id); 0861 if (found.first) 0862 { 0863 if (!overwrite && found.second.mut) 0864 return { false, i18n("Catalog already exists in the database!") }; 0865 0866 auto success = remove_catalog_force(id); 0867 if (!success.first) 0868 return success; 0869 } 0870 } 0871 0872 m_db.transaction(); 0873 0874 if (!query.exec( 0875 "INSERT INTO catalogs (id, name, mut, enabled, precedence, author, source, " 0876 "description, version, color, license, maintainer, timestamp) SELECT id, " 0877 "name, mut, enabled, precedence, author, source, description, version, " 0878 "color, license, maintainer, timestamp FROM tmp.catalogs LIMIT 1") || 0879 !query.exec(QString("CREATE TABLE cat_%1 AS SELECT * FROM tmp.cat").arg(id))) 0880 return { false, 0881 i18n("Could not import the catalog.<br>%1", query.lastError().text()) }; 0882 0883 m_db.commit(); 0884 0885 if (!update_catalog_views() || !compile_master_catalog()) 0886 return { false, i18n("Could not refresh the master catalog.<br>", 0887 m_db.lastError().text()) }; 0888 0889 return { true, {} }; 0890 } 0891 0892 std::pair<bool, QString> DBManager::remove_catalog(const int id) 0893 { 0894 if (id == SqlStatements::user_catalog_id) 0895 return { false, i18n("Removing the user catalog is not allowed.") }; 0896 0897 return remove_catalog_force(id); 0898 } 0899 0900 std::pair<bool, QString> DBManager::remove_catalog_force(const int id) 0901 { 0902 auto success = set_catalog_enabled(id, false); 0903 if (!success.first) 0904 return success; 0905 0906 QSqlQuery remove_catalog{ m_db }; 0907 remove_catalog.prepare(SqlStatements::remove_catalog); 0908 remove_catalog.bindValue(0, id); 0909 0910 m_db.transaction(); 0911 0912 if (!remove_catalog.exec() || !remove_catalog.exec(SqlStatements::drop_catalog(id))) 0913 { 0914 m_db.rollback(); 0915 return { false, i18n("Could not remove the catalog from the registry.<br>%1") 0916 .arg(remove_catalog.lastError().text()) }; 0917 } 0918 0919 m_db.commit(); 0920 // we don't have to refresh the master catalog because the disable 0921 // call already did this 0922 0923 return { true, {} }; 0924 } 0925 0926 std::pair<bool, QString> DBManager::copy_objects(const int id_1, const int id_2) 0927 { 0928 if (!(catalog_exists(id_1) && catalog_exists(id_2))) 0929 return { false, i18n("Both catalogs have to exist!") }; 0930 0931 if (!get_catalog(id_2).second.mut) 0932 return { false, i18n("Destination catalog has to be mutable!") }; 0933 0934 QSqlQuery query{ m_db }; 0935 0936 if (!query.exec(SqlStatements::move_objects(id_1, id_2))) 0937 return { false, query.lastError().text() }; 0938 0939 if (!query.exec(SqlStatements::set_catalog_all_objects(id_2))) 0940 return { false, query.lastError().text() }; 0941 0942 return { true, {} }; 0943 } 0944 0945 std::pair<bool, QString> DBManager::update_catalog_meta(const Catalog &cat) 0946 { 0947 if (!catalog_exists(cat.id)) 0948 return { false, i18n("Cannot update nonexisting catalog.") }; 0949 0950 QSqlQuery query{ m_db }; 0951 0952 query.prepare(SqlStatements::update_catalog_meta); 0953 query.bindValue(":name", cat.name); 0954 query.bindValue(":author", cat.author); 0955 query.bindValue(":source", cat.source); 0956 query.bindValue(":description", cat.description); 0957 query.bindValue(":id", cat.id); 0958 query.bindValue(":color", cat.color); 0959 query.bindValue(":license", cat.license); 0960 query.bindValue(":maintainer", cat.maintainer); 0961 query.bindValue(":timestamp", cat.timestamp); 0962 0963 return { query.exec(), query.lastError().text() }; 0964 } 0965 0966 int DBManager::find_suitable_catalog_id() 0967 { 0968 const auto &cats = get_catalogs(true); 0969 0970 // find a gap in the ids to use 0971 const auto element = std::adjacent_find( 0972 cats.cbegin(), cats.cend(), [](const auto & c1, const auto & c2) 0973 { 0974 return (c1.id >= CatalogsDB::custom_cat_min_id) && 0975 (c2.id >= CatalogsDB::custom_cat_min_id) && (c2.id - c1.id) > 1; 0976 }); 0977 0978 return std::max(CatalogsDB::custom_cat_min_id, 0979 (element == cats.cend() ? cats.back().id : element->id) + 1); 0980 } 0981 0982 QString CatalogsDB::dso_db_path() 0983 { 0984 return QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)) 0985 .filePath(Options::dSOCatalogFilename()); 0986 } 0987 0988 std::pair<bool, Catalog> CatalogsDB::read_catalog_meta_from_file(const QString &path) 0989 { 0990 QSqlDatabase db{ QSqlDatabase::addDatabase( 0991 "QSQLITE", QString("tmp_%1_%2").arg(path).arg(get_connection_index())) }; 0992 db.setDatabaseName(path); 0993 0994 if (!db.open()) 0995 return { false, {} }; 0996 0997 QSqlQuery query{ db }; 0998 0999 if (!query.exec("PRAGMA user_version") || !query.next() || 1000 query.value(0).toInt() < SqlStatements::current_db_version) 1001 { 1002 QTemporaryDir tmp; 1003 const auto new_path = tmp.filePath("cat.kscat"); 1004 1005 QFile::copy(path, new_path); 1006 db.close(); 1007 1008 db.setDatabaseName(new_path); 1009 if (!db.open()) 1010 return { false, {} }; 1011 1012 const auto &success = migrate_db(query.value(0).toInt(), db); 1013 if (!success.first) 1014 return { false, {} }; 1015 } 1016 1017 if (!query.exec(SqlStatements::get_first_catalog) || !query.first()) 1018 return { false, {} }; 1019 1020 db.close(); 1021 return { true, read_catalog(query) }; 1022 } 1023 1024 CatalogStatistics read_statistics(QSqlQuery &query) 1025 { 1026 CatalogStatistics stats{}; 1027 while (query.next()) 1028 { 1029 stats.object_counts[(SkyObject::TYPE)query.value(0).toInt()] = 1030 query.value(1).toInt(); 1031 stats.total_count += query.value(1).toInt(); 1032 } 1033 return stats; 1034 } 1035 1036 const std::pair<bool, CatalogStatistics> DBManager::get_master_statistics() 1037 { 1038 QSqlQuery query{ m_db }; 1039 if (!query.exec(SqlStatements::dso_count_by_type_master)) 1040 return { false, {} }; 1041 1042 return { true, read_statistics(query) }; 1043 } 1044 1045 const std::pair<bool, CatalogStatistics> 1046 DBManager::get_catalog_statistics(const int catalog_id) 1047 { 1048 QSqlQuery query{ m_db }; 1049 if (!query.exec(SqlStatements::dso_count_by_type(catalog_id))) 1050 return { false, {} }; 1051 1052 return { true, read_statistics(query) }; 1053 } 1054 1055 std::pair<bool, QString> 1056 CatalogsDB::DBManager::add_objects(const int catalog_id, 1057 const CatalogObjectVector &objects) 1058 { 1059 { 1060 const auto &success = get_catalog(catalog_id); 1061 if (!success.first) 1062 return { false, i18n("Catalog with id=%1 not found.", catalog_id) }; 1063 1064 if (!success.second.mut) 1065 return { false, i18n("Catalog is immutable!") }; 1066 } 1067 1068 m_db.transaction(); 1069 QSqlQuery query{ m_db }; 1070 for (const auto &object : objects) 1071 { 1072 SkyPoint tmp{ object.ra(), object.dec() }; 1073 const auto trixel = SkyMesh::Create(m_htmesh_level)->index(&tmp); 1074 1075 bind_catalogobject(query, catalog_id, object, trixel); 1076 1077 if (!query.exec()) 1078 { 1079 auto err = query.lastError().text(); 1080 if (err.startsWith("UNIQUE")) 1081 err = i18n("The object is already in the catalog!"); 1082 1083 return { false, i18n("Could not insert object! %1", err) }; 1084 } 1085 } 1086 1087 return { m_db.commit() &&update_catalog_views() &&compile_master_catalog(), 1088 m_db.lastError().text() }; 1089 }; 1090 1091 CatalogObjectList CatalogsDB::DBManager::find_objects_by_wildcard(const QString &wildcard, 1092 const int limit) 1093 { 1094 QMutexLocker _{ &m_mutex }; 1095 1096 QSqlQuery query{ m_db }; 1097 if (!query.prepare(SqlStatements::dso_by_wildcard())) 1098 { 1099 return {}; 1100 } 1101 query.bindValue(":wildcard", wildcard); 1102 query.bindValue(":limit", limit); 1103 1104 return fetch_objects(query); 1105 }; 1106 1107 std::tuple<bool, const QString, CatalogObjectList> 1108 CatalogsDB::DBManager::general_master_query(const QString &where, const QString &order_by, 1109 const int limit) 1110 { 1111 QMutexLocker _{ &m_mutex }; 1112 1113 QSqlQuery query{ m_db }; 1114 1115 if (!query.prepare(SqlStatements::dso_general_query(where, order_by))) 1116 { 1117 return { false, query.lastError().text(), {} }; 1118 } 1119 1120 query.bindValue(":limit", limit); 1121 1122 return { false, "", fetch_objects(query) }; 1123 }; 1124 1125 CatalogsDB::CatalogColorMap CatalogsDB::parse_color_string(const QString &str) 1126 { 1127 CatalogsDB::CatalogColorMap colors{}; 1128 if (str == "") 1129 return colors; 1130 1131 const auto &parts = str.split(";"); 1132 auto it = parts.constBegin(); 1133 1134 if (it->length() > 0) // playing it save 1135 colors["default"] = *it; 1136 1137 while (it != parts.constEnd()) 1138 { 1139 const auto &scheme = *(++it); 1140 if (it != parts.constEnd()) 1141 { 1142 const auto next = ++it; 1143 if (next == parts.constEnd()) 1144 break; 1145 1146 const auto &color = *next; 1147 colors[scheme] = QColor(color); 1148 } 1149 } 1150 1151 return colors; 1152 } 1153 1154 QString get_name(const QColor &color) 1155 { 1156 return color.isValid() ? color.name() : ""; 1157 } 1158 1159 QString CatalogsDB::to_color_string(CatalogColorMap colors) 1160 { 1161 QStringList color_list; 1162 1163 color_list << colors["default"].name(); 1164 colors.erase("default"); 1165 1166 for (const auto &item : colors) 1167 { 1168 if (item.second.isValid()) 1169 { 1170 color_list << item.first << item.second.name(); 1171 } 1172 } 1173 1174 return color_list.join(";"); 1175 } 1176 1177 ColorMap CatalogsDB::DBManager::get_catalog_colors() 1178 { 1179 // no mutex b.c. this is read only 1180 QSqlQuery query{ m_db }; 1181 1182 ColorMap colors{}; 1183 1184 if (!query.exec(SqlStatements::get_colors)) 1185 return colors; 1186 1187 for (const auto &cat : DBManager::get_catalogs(true)) 1188 { 1189 colors[cat.id] = parse_color_string(cat.color); 1190 } 1191 1192 while (query.next()) 1193 { 1194 const auto &catalog = query.value("catalog").toInt(); 1195 const auto &scheme = query.value("scheme").toString(); 1196 const auto &color = query.value("color").toString(); 1197 colors[catalog][scheme] = QColor(color); 1198 } 1199 1200 return colors; 1201 }; 1202 1203 CatalogsDB::CatalogColorMap CatalogsDB::DBManager::get_catalog_colors(const int id) 1204 { 1205 return get_catalog_colors()[id]; // good enough for now 1206 }; 1207 1208 std::pair<bool, QString> 1209 CatalogsDB::DBManager::insert_catalog_colors(const int id, const CatalogColorMap &colors) 1210 { 1211 QMutexLocker _{ &m_mutex }; 1212 1213 QSqlQuery query{ m_db }; 1214 1215 if (!query.prepare(SqlStatements::insert_color)) 1216 { 1217 return { false, query.lastError().text() }; 1218 } 1219 1220 query.bindValue(":catalog", id); 1221 for (const auto &item : colors) 1222 { 1223 query.bindValue(":scheme", item.first); 1224 query.bindValue(":color", item.second); 1225 1226 if (!query.exec()) 1227 return { false, query.lastError().text() }; 1228 } 1229 1230 return { true, "" }; 1231 };