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 &timestamp)
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 };