Kstars

catalogsdb.cpp
1 /*
2  SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include <limits>
8 #include <cmath>
9 #include <QSqlDriver>
10 #include <QSqlRecord>
11 #include <QMutexLocker>
12 #include <QTemporaryDir>
13 #include <qsqldatabase.h>
14 #include "cachingdms.h"
15 #include "catalogsdb.h"
16 #include "kspaths.h"
17 #include "skymesh.h"
18 #include "Options.h"
19 #include "final_action.h"
20 #include "sqlstatements.cpp"
21 
22 using namespace CatalogsDB;
23 QSet<QString> DBManager::m_db_paths{};
24 
25 /**
26  * Get an increasing index for new connections.
27  */
28 int get_connection_index()
29 {
30  static int connection_index = 0;
31  return connection_index++;
32 }
33 
34 QSqlQuery make_query(QSqlDatabase &db, const QString &statement, const bool forward_only)
35 {
36  QSqlQuery query{ db };
37 
38  query.setForwardOnly(forward_only);
39  if (!query.prepare(statement))
40  {
41  throw DatabaseError("Can't prepare query!", DatabaseError::ErrorType::PREPARE,
42  query.lastError());
43  };
44 
45  return query;
46 }
47 
48 /**
49  * Migrate the database from \p version to the current version.
50  */
51 std::pair<bool, QString> migrate_db(const int version, QSqlDatabase &db,
52  QString prefix = "")
53 {
54  if (prefix.size() > 0)
55  prefix += ".";
56 
57  // we have to add the timestamp collumn to the catalogs
58  if (version < 2)
59  {
60  QSqlQuery add_ts{ db };
61  const auto success = add_ts.exec(QString("ALTER TABLE %1catalogs ADD COLUMN "
62  "timestamp DEFAULT NULL")
63  .arg(prefix));
64  if (!success)
65  return { false, add_ts.lastError().text() };
66  }
67 
68  // adding the color selector table; this only applies for the
69  // master database
70  if (version < 3 && prefix == "")
71  {
72  QSqlQuery add_colors{ db };
73  const auto success = add_colors.exec(SqlStatements::create_colors_table);
74  if (!success)
75  return { false, add_colors.lastError().text() };
76  }
77 
78  return { true, "" };
79 }
80 
81 DBManager::DBManager(const QString &filename)
83  "QSQLITE", QString("cat_%1_%2").arg(filename).arg(get_connection_index())) },
84  m_db_file{ *m_db_paths.insert(filename) }
85 
86 {
87  m_db.setDatabaseName(m_db_file);
88 
89  // we are throwing here, because errors at this stage should be fatal
90  if (!m_db.open())
91  {
92  throw DatabaseError(QString("Cannot open CatalogDatabase '%1'!").arg(m_db_file),
93  DatabaseError::ErrorType::OPEN, m_db.lastError());
94  }
95 
96  bool init = false;
97  std::tie(m_db_version, m_htmesh_level, init) = get_db_meta();
98 
99  if (!init && m_db_version > 0 && m_db_version < SqlStatements::current_db_version)
100  {
101  const auto &backup_path = QString("%1.%2").arg(m_db_file).arg(
102  QDateTime::currentDateTime().toString("dd_MMMM_yy_hh_mm_sss_zzz"));
103 
104  if (!QFile::copy(m_db_file, backup_path))
105  {
106  throw DatabaseError(
107  QString("Could not backup dso database before upgrading."),
108  DatabaseError::ErrorType::VERSION, QSqlError{});
109  }
110 
111  const auto &success = migrate_db(m_db_version, m_db);
112  if (success.first)
113  {
114  m_db_version = SqlStatements::current_db_version;
115  QSqlQuery version_query{ m_db };
116  version_query.prepare(SqlStatements::update_version);
117  version_query.bindValue(":version", m_db_version);
118 
119  if (!version_query.exec())
120  {
121  throw DatabaseError(QString("Could not update the database version."),
122  DatabaseError::ErrorType::VERSION,
123  version_query.lastError());
124  }
125  }
126  else
127  throw DatabaseError(
128  QString("Wrong database version. Expected %1 and got %2 and "
129  "migration is not possible.")
130  .arg(SqlStatements::current_db_version)
131  .arg(m_db_version),
132  DatabaseError::ErrorType::VERSION, success.second);
133  }
134 
135  QSqlQuery master_exists{ m_db };
136  master_exists.exec(SqlStatements::exists_master);
137  const bool master_does_exist = master_exists.next();
138  master_exists.finish();
139 
140  if (init || !master_does_exist)
141  {
142  if (!initialize_db())
143  {
144  throw DatabaseError(QString("Could not initialize database."),
145  DatabaseError::ErrorType::INIT, m_db.lastError());
146  }
147 
148  if (!catalog_exists(SqlStatements::user_catalog_id))
149  {
150  const auto &res =
151  register_catalog(SqlStatements::user_catalog_id,
152  SqlStatements::user_catalog_name, true, true, 1);
153  if (!res.first)
154  {
155  throw DatabaseError(QString("Could not create user database."),
156  DatabaseError::ErrorType::CREATE_CATALOG, res.second);
157  }
158  }
159 
160  if (!update_catalog_views())
161  {
162  throw DatabaseError(QString("Unable to create combined catalog view!"),
163  DatabaseError::ErrorType::CREATE_CATALOG,
164  m_db.lastError());
165  }
166 
167  if (!compile_master_catalog())
168  {
169  throw DatabaseError(QString("Unable to create master catalog!"),
170  DatabaseError::ErrorType::CREATE_MASTER,
171  m_db.lastError());
172  }
173  }
174 
175  m_q_cat_by_id = make_query(m_db, SqlStatements::get_catalog_by_id, true);
176  m_q_obj_by_trixel = make_query(m_db, SqlStatements::dso_by_trixel, false);
177  m_q_obj_by_trixel_no_nulls = make_query(m_db, SqlStatements::dso_by_trixel_no_nulls, false);
178  m_q_obj_by_trixel_null_mag = make_query(m_db, SqlStatements::dso_by_trixel_null_mag, false);
179  m_q_obj_by_name = make_query(m_db, SqlStatements::dso_by_name, true);
180  m_q_obj_by_name_exact = make_query(m_db, SqlStatements::dso_by_name_exact, true);
181  m_q_obj_by_lim = make_query(m_db, SqlStatements::dso_by_lim, true);
182  m_q_obj_by_maglim = make_query(m_db, SqlStatements::dso_by_maglim, true);
183  m_q_obj_by_maglim_and_type =
184  make_query(m_db, SqlStatements::dso_by_maglim_and_type, true);
185  m_q_obj_by_oid = make_query(m_db, SqlStatements::dso_by_oid, true);
186 };
187 
188 DBManager::DBManager(const DBManager &other) : DBManager::DBManager{ other.m_db_file } {};
189 
190 bool DBManager::initialize_db()
191 {
192  if (m_db_version < 0 || m_htmesh_level < 1)
193  throw std::runtime_error("DBManager not initialized properly, m_db_vesion and "
194  "m_htmesh_level have to be set.");
195 
196  if (!m_db.exec(SqlStatements::create_meta_table).isActive())
197  return false;
198 
199  if (!m_db.exec(SqlStatements::create_colors_table).isActive())
200  return false;
201 
202  QSqlQuery meta_query{ m_db };
203  meta_query.prepare(SqlStatements::set_meta);
204  meta_query.bindValue(0, m_db_version);
205  meta_query.bindValue(1, m_htmesh_level);
206  meta_query.bindValue(2, false);
207 
208  if (!meta_query.exec())
209  return false;
210 
211  return m_db.exec(SqlStatements::create_catalog_list_table).isActive();
212 }
213 
214 std::tuple<int, int, bool> DBManager::get_db_meta()
215 {
216  auto query = m_db.exec(SqlStatements::get_meta);
217 
218  if (query.first())
219  return { query.value(0).toInt(), query.value(1).toInt(),
220  query.value(2).toBool() };
221  else
222  return { SqlStatements::current_db_version, SqlStatements::default_htmesh_level,
223  true };
224 }
225 
226 std::vector<int> DBManager::get_catalog_ids(bool include_disabled)
227 {
228  auto query = m_db.exec(include_disabled ? SqlStatements::get_all_catalog_ids :
229  SqlStatements::get_catalog_ids);
230 
231  std::vector<int> ids;
232 
233  while (query.next())
234  {
235  int id = query.value(0).toInt();
236  ids.push_back(id);
237  }
238 
239  return ids;
240 }
241 
243 {
244  const auto &ids = get_catalog_ids();
245  bool result = true;
246  auto _ = gsl::finally([&]() { m_db.commit(); });
247 
248  m_db.transaction();
249  QSqlQuery query{ m_db };
250  result &=
251  query.exec(QString("DROP VIEW IF EXISTS ") + SqlStatements::all_catalog_view);
252 
253  if (!result)
254  {
255  return result;
256  }
257 
258  QString view{
259  "CREATE VIEW "
260  }; // small enough to be included here and not in sqlstatements
261 
262  view += SqlStatements::all_catalog_view;
263  view += " AS\n";
264 
265  QStringList prefixed{};
266  for (auto *field : SqlStatements::catalog_collumns)
267  {
268  prefixed << QString("c.") + field;
269  }
270 
271  QString prefixed_joined = prefixed.join(",");
272 
273  QStringList catalog_queries{};
274  for (auto id : ids)
275  {
276  catalog_queries << SqlStatements::all_catalog_view_body(
277  prefixed_joined, SqlStatements::catalog_prefix, id);
278  }
279 
280  if (ids.size() == 0)
281  {
282  catalog_queries << SqlStatements::all_catalog_view_body(
283  prefixed_joined, SqlStatements::catalog_prefix, 0) +
284  " WHERE FALSE"; // we blackhole the query
285  }
286 
287  view += catalog_queries.join("\nUNION ALL\n");
288  result &= query.exec(view);
289  return result;
290 }
291 
292 void bind_catalog(QSqlQuery &query, const Catalog &cat)
293 {
294  query.bindValue(":id", cat.id);
295  query.bindValue(":name", cat.name);
296  query.bindValue(":mut", cat.mut);
297  query.bindValue(":enabled", cat.enabled);
298  query.bindValue(":precedence", cat.precedence);
299  query.bindValue(":author", cat.author);
300  query.bindValue(":source", cat.source);
301  query.bindValue(":description", cat.description);
302  query.bindValue(":version", cat.version);
303  query.bindValue(":color", cat.color);
304  query.bindValue(":license", cat.license);
305  query.bindValue(":maintainer", cat.maintainer);
306  query.bindValue(":timestamp", cat.timestamp);
307 }
308 
309 std::pair<bool, QString> DBManager::register_catalog(
310  const int id, const QString &name, const bool mut, const bool enabled,
311  const double precedence, const QString &author, const QString &source,
312  const QString &description, const int version, const QString &color,
313  const QString &license, const QString &maintainer, const QDateTime &timestamp)
314 {
315  return register_catalog({ id, name, precedence, author, source, description, mut,
316  enabled, version, color, license, maintainer, timestamp });
317 }
318 
319 std::pair<bool, QString> DBManager::register_catalog(const Catalog &cat)
320 {
321  if (catalog_exists(cat.id))
322  return { false, i18n("Catalog with that ID already exists.") };
323 
324  QSqlQuery query{ m_db };
325 
326  if (!query.exec(SqlStatements::create_catalog_table(cat.id)))
327  {
328  return { false, query.lastError().text() };
329  }
330 
331  query.prepare(SqlStatements::insert_catalog);
332  bind_catalog(query, cat);
333 
334  return { query.exec(), query.lastError().text() };
335 };
336 
338 {
339  auto _ = gsl::finally([&]() { m_db.commit(); });
340  QSqlQuery query{ m_db };
341  m_db.transaction();
342 
343  if (!query.exec(SqlStatements::drop_master))
344  {
345  return false;
346  }
347 
348  if (!query.exec(SqlStatements::create_master))
349  {
350  return false;
351  }
352 
353  bool success = true;
354  success &= query.exec(SqlStatements::create_master_trixel_index);
355  success &= query.exec(SqlStatements::create_master_mag_index);
356  success &= query.exec(SqlStatements::create_master_type_index);
357  success &= query.exec(SqlStatements::create_master_name_index);
358  return success;
359 };
360 
361 const Catalog read_catalog(const QSqlQuery &query)
362 {
363  return { query.value("id").toInt(),
364  query.value("name").toString(),
365  query.value("precedence").toDouble(),
366  query.value("author").toString(),
367  query.value("source").toString(),
368  query.value("description").toString(),
369  query.value("mut").toBool(),
370  query.value("enabled").toBool(),
371  query.value("version").toInt(),
372  query.value("color").toString(),
373  query.value("license").toString(),
374  query.value("maintainer").toString(),
375  query.value("timestamp").toDateTime() };
376 }
377 
378 const std::pair<bool, Catalog> DBManager::get_catalog(const int id)
379 {
380  QMutexLocker _{ &m_mutex };
381  m_q_cat_by_id.bindValue(0, id);
382 
383  if (!m_q_cat_by_id.exec())
384  return { false, {} };
385 
386  if (!m_q_cat_by_id.next())
387  return { false, {} };
388 
389  Catalog cat{ read_catalog(m_q_cat_by_id) };
390 
391  m_q_cat_by_id.finish();
392  return { true, cat };
393 }
394 
395 bool DBManager::catalog_exists(const int id)
396 {
397  QMutexLocker _{ &m_mutex };
398  m_q_cat_by_id.bindValue(0, id);
399  auto end = gsl::finally([&]() { m_q_cat_by_id.finish(); });
400 
401  if (!m_q_cat_by_id.exec())
402  return false;
403 
404  return m_q_cat_by_id.next();
405 }
406 
407 size_t count_rows(QSqlQuery &query)
408 {
409  size_t count{ 0 };
410  while (query.next())
411  {
412  count++;
413  }
414 
415  return count;
416 }
417 
418 CatalogObject DBManager::read_catalogobject(const QSqlQuery &query) const
419 {
420  const CatalogObject::oid id = query.value(0).toByteArray();
421  const SkyObject::TYPE type = static_cast<SkyObject::TYPE>(query.value(1).toInt());
422 
423  const double ra = query.value(2).toDouble();
424  const double dec = query.value(3).toDouble();
425  const float mag = query.isNull(4) ? NaN::f : query.value(4).toFloat();
426  const QString name = query.value(5).toString();
427  const QString long_name = query.value(6).toString();
428  const QString catalog_identifier = query.value(7).toString();
429  const float major = query.value(8).toFloat();
430  const float minor = query.value(9).toFloat();
431  const double position_angle = query.value(10).toDouble();
432  const float flux = query.value(11).toFloat();
433  const int catalog_id = query.value(12).toInt();
434 
435  return { id, type, dms(ra), dms(dec),
436  mag, name, long_name, catalog_identifier,
437  catalog_id, major, minor, position_angle,
438  flux, m_db_file };
439 }
440 
441 CatalogObjectVector DBManager::_get_objects_in_trixel_generic(QSqlQuery& query, const int trixel)
442 {
443  QMutexLocker _{ &m_mutex }; // this costs ~ .1ms which is ok
444  query.bindValue(0, trixel);
445 
446  if (!query.exec()) // we throw because this is not recoverable
447  throw DatabaseError(
448  QString("The by-trixel query for objects in trixel=%1 failed.")
449  .arg(trixel),
450  DatabaseError::ErrorType::UNKNOWN, query.lastError());
451 
452  CatalogObjectVector objects;
453  size_t count =
454  count_rows(query); // this also moves the query head to the end
455 
456  if (count == 0)
457  {
458  query.finish();
459  return objects;
460  }
461 
462  objects.reserve(count);
463 
464  while (query.previous())
465  {
466  objects.push_back(read_catalogobject(query));
467  }
468 
469  query.finish();
470 
471  // move semantics baby!
472  return objects;
473 }
474 
475 CatalogObjectList DBManager::fetch_objects(QSqlQuery &query) const
476 {
477  CatalogObjectList objects;
478  auto _ = gsl::finally([&]() { query.finish(); });
479 
480  query.exec();
481 
482  if (!query.isActive())
483  return {};
484  while (query.next())
485  objects.push_back(read_catalogobject(query));
486 
487  return objects;
488 }
489 
490 CatalogObjectList DBManager::find_objects_by_name(const QString &name, const int limit,
491  const bool exactMatchOnly)
492 {
493  QMutexLocker _{ &m_mutex };
494 
495  // limit < 0 is a sentinel value for unlimited
496  if (limit == 0)
497  return CatalogObjectList();
498 
499  // search for an exact match first
500  m_q_obj_by_name_exact.bindValue(":name", name);
501  CatalogObjectList objs { fetch_objects(m_q_obj_by_name_exact) };
502 
503  if ((limit == 1 && objs.size() > 0) || exactMatchOnly)
504  return objs;
505 
506  Q_ASSERT(objs.size() <= 1);
507 
508  m_q_obj_by_name.bindValue(":name", name);
509  m_q_obj_by_name.bindValue(":limit", int(limit - objs.size()));
510 
511  CatalogObjectList moreObjects = fetch_objects(m_q_obj_by_name);
512  moreObjects.splice(moreObjects.begin(), objs);
513  return moreObjects;
514 
515 }
516 
517 CatalogObjectList DBManager::find_objects_by_name(const int catalog_id,
518  const QString &name, const int limit)
519 {
520  QSqlQuery query{ m_db };
521 
522  query.prepare(SqlStatements::dso_by_name_and_catalog(catalog_id));
523  query.bindValue(":name", name);
524  query.bindValue(":limit", limit);
525  query.bindValue(":catalog", catalog_id);
526 
527  return fetch_objects(query);
528 }
529 
530 std::pair<bool, CatalogObject> DBManager::read_first_object(QSqlQuery &query) const
531 {
532  if (!query.exec() || !query.first())
533  return { false, {} };
534 
535  return { true, read_catalogobject(query) };
536 }
537 
538 std::pair<bool, CatalogObject> DBManager::get_object(const CatalogObject::oid &oid)
539 {
540  QMutexLocker _{ &m_mutex };
541  m_q_obj_by_oid.bindValue(0, oid);
542 
543  auto f = gsl::finally([&]() { // taken from the GSL, runs when it goes out of scope
544  m_q_obj_by_oid.finish();
545  });
546 
547  return read_first_object(m_q_obj_by_oid);
548 };
549 
550 std::pair<bool, CatalogObject> DBManager::get_object(const CatalogObject::oid &oid,
551  const int catalog_id)
552 {
553  QMutexLocker _{ &m_mutex };
554  QSqlQuery query{ m_db };
555 
556  query.prepare(SqlStatements::dso_by_oid_and_catalog(catalog_id));
557  query.bindValue(0, oid);
558 
559  return read_first_object(query);
560 };
561 
562 CatalogObjectList DBManager::get_objects(float maglim, int limit)
563 {
564  QMutexLocker _{ &m_mutex };
565  m_q_obj_by_maglim.bindValue(":maglim", maglim);
566  m_q_obj_by_maglim.bindValue(":limit", limit);
567 
568  return fetch_objects(m_q_obj_by_maglim);
569 }
570 
571 CatalogObjectList DBManager::get_objects_all()
572 {
573  QMutexLocker _{ &m_mutex };
574  m_q_obj_by_lim.bindValue(":limit", -1);
575 
576  return fetch_objects(m_q_obj_by_lim);
577 }
578 
579 CatalogObjectList DBManager::get_objects(SkyObject::TYPE type, float maglim, int limit)
580 {
581  QMutexLocker _{ &m_mutex };
582  m_q_obj_by_maglim_and_type.bindValue(":type", type);
583  m_q_obj_by_maglim_and_type.bindValue(":limit", limit);
584  m_q_obj_by_maglim_and_type.bindValue(":maglim", maglim);
585 
586  return fetch_objects(m_q_obj_by_maglim_and_type);
587 }
588 
590  const int catalog_id, float maglim,
591  int limit)
592 {
593  QSqlQuery query{ m_db };
594 
595  query.prepare(SqlStatements::dso_in_catalog_by_maglim(catalog_id));
596  query.bindValue(":type", type);
597  query.bindValue(":limit", limit);
598  query.bindValue(":maglim", maglim);
599  return fetch_objects(query);
600 }
601 
602 std::pair<bool, QString> DBManager::set_catalog_enabled(const int id, const bool enabled)
603 {
604  const auto &success = get_catalog(id);
605  if (!success.first)
606  return { false, i18n("Catalog could not be found.") };
607 
608  const auto &cat = success.second;
609  if (cat.enabled == enabled)
610  return { true, "" };
611 
612  QSqlQuery query{ m_db };
613  query.prepare(SqlStatements::enable_disable_catalog);
614  query.bindValue(":enabled", enabled);
615  query.bindValue(":id", id);
616 
617  return { query.exec() && update_catalog_views() && compile_master_catalog(),
618  query.lastError().text() + m_db.lastError().text() };
619 }
620 
621 const std::vector<Catalog> DBManager::get_catalogs(bool include_disabled)
622 {
623  auto ids = get_catalog_ids(include_disabled);
624  std::vector<Catalog> catalogs;
625  catalogs.reserve(ids.size());
626 
627  std::transform(ids.cbegin(), ids.cend(), std::back_inserter(catalogs),
628  [&](const int id) {
629  const auto &found = get_catalog(id);
630  if (found.first)
631  return found.second;
632 
633  // This really should **not** happen
634  throw DatabaseError(
635  QString("Could not retrieve the catalog with id=%1").arg(id));
636  });
637 
638  return catalogs;
639 }
640 
641 inline void bind_catalogobject(QSqlQuery &query, const int catalog_id,
642  const SkyObject::TYPE t, const CachingDms &r,
643  const CachingDms &d, const QString &n, const float m,
644  const QString &lname, const QString &catalog_identifier,
645  const float a, const float b, const double pa,
646  const float flux, Trixel trixel,
647  const CatalogObject::oid &new_id)
648 {
649  query.prepare(SqlStatements::insert_dso(catalog_id));
650 
651  query.bindValue(":hash", new_id); // no dedupe, maybe in the future
652  query.bindValue(":oid", new_id);
653  query.bindValue(":type", static_cast<int>(t));
654  query.bindValue(":ra", r.Degrees());
655  query.bindValue(":dec", d.Degrees());
656  query.bindValue(":magnitude", (m < 99 && !std::isnan(m)) ? m : QVariant{});
657  query.bindValue(":name", n);
658  query.bindValue(":long_name", lname.length() > 0 ? lname : QVariant{});
659  query.bindValue(":catalog_identifier",
660  catalog_identifier.length() > 0 ? catalog_identifier : QVariant{});
661  query.bindValue(":major_axis", a > 0 ? a : QVariant{});
662  query.bindValue(":minor_axis", b > 0 ? b : QVariant{});
663  query.bindValue(":position_angle", pa > 0 ? pa : QVariant{});
664  query.bindValue(":flux", flux > 0 ? flux : QVariant{});
665  query.bindValue(":trixel", trixel);
666  query.bindValue(":catalog", catalog_id);
667 }
668 
669 inline void bind_catalogobject(QSqlQuery &query, const int catalog_id,
670  const CatalogObject &obj, Trixel trixel)
671 {
672  bind_catalogobject(query, catalog_id, static_cast<SkyObject::TYPE>(obj.type()),
673  obj.ra0(), obj.dec0(), obj.name(), obj.mag(), obj.longname(),
674  obj.catalogIdentifier(), obj.a(), obj.b(), obj.pa(), obj.flux(),
675  trixel, obj.getObjectId());
676 };
677 
678 std::pair<bool, QString> DBManager::add_object(const int catalog_id,
679  const CatalogObject &obj)
680 {
681  return add_object(catalog_id, static_cast<SkyObject::TYPE>(obj.type()), obj.ra0(),
682  obj.dec0(), obj.name(), obj.mag(), obj.longname(),
683  obj.catalogIdentifier(), obj.a(), obj.b(), obj.pa(), obj.flux());
684 }
685 
686 std::pair<bool, QString>
687 DBManager::add_object(const int catalog_id, const SkyObject::TYPE t, const CachingDms &r,
688  const CachingDms &d, const QString &n, const float m,
689  const QString &lname, const QString &catalog_identifier,
690  const float a, const float b, const double pa, const float flux)
691 {
692  {
693  const auto &success = get_catalog(catalog_id);
694  if (!success.first)
695  return { false, i18n("Catalog with id=%1 not found.", catalog_id) };
696 
697  if (!success.second.mut)
698  return { false, i18n("Catalog is immutable!") };
699  }
700 
701  SkyPoint tmp{ r, d };
702  const auto trixel = SkyMesh::Create(m_htmesh_level)->index(&tmp);
703  QSqlQuery query{ m_db };
704 
705  const auto new_id =
706  CatalogObject::getId(t, r.Degrees(), d.Degrees(), n, catalog_identifier);
707  bind_catalogobject(query, catalog_id, t, r, d, n, m, lname, catalog_identifier, a, b,
708  pa, flux, trixel, new_id);
709 
710  if (!query.exec())
711  {
712  auto err = query.lastError().text();
713  if (err.startsWith("UNIQUE"))
714  err = i18n("The object is already in the catalog!");
715 
716  return { false, i18n("Could not insert object! %1", err) };
717  }
718 
720  m_db.lastError().text() };
721 }
722 
723 std::pair<bool, QString> DBManager::remove_object(const int catalog_id,
724  const CatalogObject::oid &id)
725 {
726  QSqlQuery query{ m_db };
727 
728  query.prepare(SqlStatements::remove_dso(catalog_id));
729  query.bindValue(":oid", id);
730 
731  if (!query.exec())
732  return { false, query.lastError().text() };
733 
735  m_db.lastError().text() };
736 }
737 
738 std::pair<bool, QString> DBManager::dump_catalog(int catalog_id, QString file_path)
739 {
740  const auto &found = get_catalog(catalog_id);
741  if (!found.first)
742  return { false, i18n("Catalog could not be found.") };
743 
744  QFile file{ file_path };
745  if (!file.open(QIODevice::WriteOnly))
746  return { false, i18n("Output file is not writable.") };
747  file.resize(0);
748  file.close();
749 
750  QSqlQuery query{ m_db };
751 
752  if (!query.exec(QString("ATTACH [%1] AS tmp").arg(file_path)))
753  return { false,
754  i18n("Could not attach output file.<br>%1", query.lastError().text()) };
755 
756  m_db.transaction();
757  auto _ = gsl::finally([&]() { // taken from the GSL, runs when it goes out of scope
758  m_db.commit();
759  query.exec("DETACH tmp");
760  });
761 
762  if (!query.exec(
763  QString("CREATE TABLE tmp.cat AS SELECT * FROM cat_%1").arg(catalog_id)))
764  return { false, i18n("Could not copy catalog to output file.<br>%1")
765  .arg(query.lastError().text()) };
766 
767  if (!query.exec(SqlStatements::create_catalog_registry("tmp.catalogs")))
768  return { false, i18n("Could not create catalog registry in output file.<br>%1")
769  .arg(query.lastError().text()) };
770 
771  query.prepare(SqlStatements::insert_into_catalog_registry("tmp.catalogs"));
772 
773  auto cat = found.second;
774  cat.enabled = true;
775  bind_catalog(query, cat);
776 
777  if (!query.exec())
778  {
779  return { false,
780  i18n("Could not insert catalog into registry in output file.<br>%1")
781  .arg(query.lastError().text()) };
782  }
783 
784  if (!query.exec(QString("PRAGMA tmp.user_version = %1").arg(m_db_version)))
785  {
786  return { false, i18n("Could not insert set exported database version.<br>%1")
787  .arg(query.lastError().text()) };
788  }
789 
790  if (!query.exec(QString("PRAGMA tmp.application_id = %1").arg(application_id)))
791  {
792  return { false,
793  i18n("Could not insert set exported database application id.<br>%1")
794  .arg(query.lastError().text()) };
795  }
796 
797  return { true, {} };
798 }
799 
800 std::pair<bool, QString> DBManager::import_catalog(const QString &file_path,
801  const bool overwrite)
802 {
803  QTemporaryDir tmp;
804  const auto new_path = tmp.filePath("cat.kscat");
805  QFile::copy(file_path, new_path);
806 
807  QFile file{ new_path };
808  if (!file.open(QIODevice::ReadOnly))
809  return { false, i18n("Catalog file is not readable.") };
810  file.close();
811 
812  QSqlQuery query{ m_db };
813 
814  if (!query.exec(QString("ATTACH [%1] AS tmp").arg(new_path)))
815  {
816  m_db.commit();
817  return { false,
818  i18n("Could not attach input file.<br>%1", query.lastError().text()) };
819  }
820 
821  auto _ = gsl::finally([&]() {
822  m_db.commit();
823  query.exec("DETACH tmp");
824  });
825 
826  if (!query.exec("PRAGMA tmp.application_id") || !query.next() ||
827  query.value(0).toInt() != CatalogsDB::application_id)
828  return { false, i18n("Invalid catalog file.") };
829 
830  if (!query.exec("PRAGMA tmp.user_version") || !query.next() ||
831  query.value(0).toInt() < m_db_version)
832  {
833  const auto &success = migrate_db(query.value(0).toInt(), m_db, "tmp");
834  if (!success.first)
835  return { false, i18n("Could not migrate old catalog format.<br>%1",
836  success.second) };
837  }
838 
839  if (!query.exec("SELECT id FROM tmp.catalogs LIMIT 1") || !query.next())
840  return { false,
841  i18n("Could read the catalog id.<br>%1", query.lastError().text()) };
842 
843  const auto id = query.value(0).toInt();
844  query.finish();
845 
846  {
847  const auto &found = get_catalog(id);
848  if (found.first)
849  {
850  if (!overwrite && found.second.mut)
851  return { false, i18n("Catalog already exists in the database!") };
852 
853  auto success = remove_catalog_force(id);
854  if (!success.first)
855  return success;
856  }
857  }
858 
859  m_db.transaction();
860 
861  if (!query.exec(
862  "INSERT INTO catalogs (id, name, mut, enabled, precedence, author, source, "
863  "description, version, color, license, maintainer, timestamp) SELECT id, "
864  "name, mut, enabled, precedence, author, source, description, version, "
865  "color, license, maintainer, timestamp FROM tmp.catalogs LIMIT 1") ||
866  !query.exec(QString("CREATE TABLE cat_%1 AS SELECT * FROM tmp.cat").arg(id)))
867  return { false,
868  i18n("Could not import the catalog.<br>%1", query.lastError().text()) };
869 
870  m_db.commit();
871 
873  return { false, i18n("Could not refresh the master catalog.<br>",
874  m_db.lastError().text()) };
875 
876  return { true, {} };
877 }
878 
879 std::pair<bool, QString> DBManager::remove_catalog(const int id)
880 {
881  if (id == SqlStatements::user_catalog_id)
882  return { false, i18n("Removing the user catalog is not allowed.") };
883 
884  return remove_catalog_force(id);
885 }
886 
887 std::pair<bool, QString> DBManager::remove_catalog_force(const int id)
888 {
889  auto success = set_catalog_enabled(id, false);
890  if (!success.first)
891  return success;
892 
893  QSqlQuery remove_catalog{ m_db };
894  remove_catalog.prepare(SqlStatements::remove_catalog);
895  remove_catalog.bindValue(0, id);
896 
897  m_db.transaction();
898 
899  if (!remove_catalog.exec() || !remove_catalog.exec(SqlStatements::drop_catalog(id)))
900  {
901  m_db.rollback();
902  return { false, i18n("Could not remove the catalog from the registry.<br>%1")
903  .arg(remove_catalog.lastError().text()) };
904  }
905 
906  m_db.commit();
907  // we don't have to refresh the master catalog because the disable
908  // call already did this
909 
910  return { true, {} };
911 }
912 
913 std::pair<bool, QString> DBManager::copy_objects(const int id_1, const int id_2)
914 {
915  if (!(catalog_exists(id_1) && catalog_exists(id_2)))
916  return { false, i18n("Both catalogs have to exist!") };
917 
918  if (!get_catalog(id_2).second.mut)
919  return { false, i18n("Destination catalog has to be mutable!") };
920 
921  QSqlQuery query{ m_db };
922 
923  if (!query.exec(SqlStatements::move_objects(id_1, id_2)))
924  return { false, query.lastError().text() };
925 
926  if (!query.exec(SqlStatements::set_catalog_all_objects(id_2)))
927  return { false, query.lastError().text() };
928 
929  return { true, {} };
930 }
931 
932 std::pair<bool, QString> DBManager::update_catalog_meta(const Catalog &cat)
933 {
934  if (!catalog_exists(cat.id))
935  return { false, i18n("Cannot update nonexisting catalog.") };
936 
937  QSqlQuery query{ m_db };
938 
939  query.prepare(SqlStatements::update_catalog_meta);
940  query.bindValue(":name", cat.name);
941  query.bindValue(":author", cat.author);
942  query.bindValue(":source", cat.source);
943  query.bindValue(":description", cat.description);
944  query.bindValue(":id", cat.id);
945  query.bindValue(":color", cat.color);
946  query.bindValue(":license", cat.license);
947  query.bindValue(":maintainer", cat.maintainer);
948  query.bindValue(":timestamp", cat.timestamp);
949 
950  return { query.exec(), query.lastError().text() };
951 }
952 
954 {
955  const auto &cats = get_catalogs(true);
956 
957  // find a gap in the ids to use
958  const auto element = std::adjacent_find(
959  cats.cbegin(), cats.cend(), [](const auto &c1, const auto &c2) {
960  return (c1.id >= CatalogsDB::custom_cat_min_id) &&
961  (c2.id >= CatalogsDB::custom_cat_min_id) && (c2.id - c1.id) > 1;
962  });
963 
964  return std::max(CatalogsDB::custom_cat_min_id,
965  (element == cats.cend() ? cats.back().id : element->id) + 1);
966 }
967 
968 QString CatalogsDB::dso_db_path()
969 {
970  return QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation))
971  .filePath(Options::dSOCatalogFilename());
972 }
973 
974 std::pair<bool, Catalog> CatalogsDB::read_catalog_meta_from_file(const QString &path)
975 {
977  "QSQLITE", QString("tmp_%1_%2").arg(path).arg(get_connection_index())) };
978  db.setDatabaseName(path);
979 
980  if (!db.open())
981  return { false, {} };
982 
983  QSqlQuery query{ db };
984 
985  if (!query.exec("PRAGMA user_version") || !query.next() ||
986  query.value(0).toInt() < SqlStatements::current_db_version)
987  {
988  QTemporaryDir tmp;
989  const auto new_path = tmp.filePath("cat.kscat");
990 
991  QFile::copy(path, new_path);
992  db.close();
993 
994  db.setDatabaseName(new_path);
995  if (!db.open())
996  return { false, {} };
997 
998  const auto &success = migrate_db(query.value(0).toInt(), db);
999  if (!success.first)
1000  return { false, {} };
1001  }
1002 
1003  if (!query.exec(SqlStatements::get_first_catalog) || !query.first())
1004  return { false, {} };
1005 
1006  db.close();
1007  return { true, read_catalog(query) };
1008 }
1009 
1010 CatalogStatistics read_statistics(QSqlQuery &query)
1011 {
1012  CatalogStatistics stats{};
1013  while (query.next())
1014  {
1015  stats.object_counts[(SkyObject::TYPE)query.value(0).toInt()] =
1016  query.value(1).toInt();
1017  stats.total_count += query.value(1).toInt();
1018  }
1019  return stats;
1020 }
1021 
1022 const std::pair<bool, CatalogStatistics> DBManager::get_master_statistics()
1023 {
1024  QSqlQuery query{ m_db };
1025  if (!query.exec(SqlStatements::dso_count_by_type_master))
1026  return { false, {} };
1027 
1028  return { true, read_statistics(query) };
1029 }
1030 
1031 const std::pair<bool, CatalogStatistics>
1033 {
1034  QSqlQuery query{ m_db };
1035  if (!query.exec(SqlStatements::dso_count_by_type(catalog_id)))
1036  return { false, {} };
1037 
1038  return { true, read_statistics(query) };
1039 }
1040 
1041 std::pair<bool, QString>
1043  const CatalogObjectVector &objects)
1044 {
1045  {
1046  const auto &success = get_catalog(catalog_id);
1047  if (!success.first)
1048  return { false, i18n("Catalog with id=%1 not found.", catalog_id) };
1049 
1050  if (!success.second.mut)
1051  return { false, i18n("Catalog is immutable!") };
1052  }
1053 
1054  m_db.transaction();
1055  QSqlQuery query{ m_db };
1056  for (const auto &object : objects)
1057  {
1058  SkyPoint tmp{ object.ra(), object.dec() };
1059  const auto trixel = SkyMesh::Create(m_htmesh_level)->index(&tmp);
1060 
1061  bind_catalogobject(query, catalog_id, object, trixel);
1062 
1063  if (!query.exec())
1064  {
1065  auto err = query.lastError().text();
1066  if (err.startsWith("UNIQUE"))
1067  err = i18n("The object is already in the catalog!");
1068 
1069  return { false, i18n("Could not insert object! %1", err) };
1070  }
1071  }
1072 
1073  return { m_db.commit() && update_catalog_views() && compile_master_catalog(),
1074  m_db.lastError().text() };
1075 };
1076 
1078  const int limit)
1079 {
1080  QMutexLocker _{ &m_mutex };
1081 
1082  QSqlQuery query{ m_db };
1083  if (!query.prepare(SqlStatements::dso_by_wildcard()))
1084  {
1085  return {};
1086  }
1087  query.bindValue(":wildcard", wildcard);
1088  query.bindValue(":limit", limit);
1089 
1090  return fetch_objects(query);
1091 };
1092 
1093 std::tuple<bool, const QString, CatalogObjectList>
1095  const int limit)
1096 {
1097  QMutexLocker _{ &m_mutex };
1098 
1099  QSqlQuery query{ m_db };
1100 
1101  if (!query.prepare(SqlStatements::dso_general_query(where, order_by)))
1102  {
1103  return { false, query.lastError().text(), {} };
1104  }
1105 
1106  query.bindValue(":limit", limit);
1107 
1108  return { false, "", fetch_objects(query) };
1109 };
1110 
1111 CatalogsDB::CatalogColorMap CatalogsDB::parse_color_string(const QString &str)
1112 {
1113  CatalogsDB::CatalogColorMap colors{};
1114  if (str == "")
1115  return colors;
1116 
1117  const auto &parts = str.split(";");
1118  auto it = parts.constBegin();
1119 
1120  if (it->length() > 0) // playing it save
1121  colors["default"] = *it;
1122 
1123  while (it != parts.constEnd())
1124  {
1125  const auto &scheme = *(++it);
1126  if (it != parts.constEnd())
1127  {
1128  const auto next = ++it;
1129  if (next == parts.constEnd())
1130  break;
1131 
1132  const auto &color = *next;
1133  colors[scheme] = QColor(color);
1134  }
1135  }
1136 
1137  return colors;
1138 }
1139 
1140 QString get_name(const QColor &color)
1141 {
1142  return color.isValid() ? color.name() : "";
1143 }
1144 
1145 QString CatalogsDB::to_color_string(CatalogColorMap colors)
1146 {
1147  QStringList color_list;
1148 
1149  color_list << colors["default"].name();
1150  colors.erase("default");
1151 
1152  for (const auto &item : colors)
1153  {
1154  if (item.second.isValid())
1155  {
1156  color_list << item.first << item.second.name();
1157  }
1158  }
1159 
1160  return color_list.join(";");
1161 }
1162 
1164 {
1165  // no mutex b.c. this is read only
1166  QSqlQuery query{ m_db };
1167 
1168  ColorMap colors{};
1169 
1170  if (!query.exec(SqlStatements::get_colors))
1171  return colors;
1172 
1173  for (const auto &cat : DBManager::get_catalogs(true))
1174  {
1175  colors[cat.id] = parse_color_string(cat.color);
1176  }
1177 
1178  while (query.next())
1179  {
1180  const auto &catalog = query.value("catalog").toInt();
1181  const auto &scheme = query.value("scheme").toString();
1182  const auto &color = query.value("color").toString();
1183  colors[catalog][scheme] = QColor(color);
1184  }
1185 
1186  return colors;
1187 };
1188 
1189 CatalogsDB::CatalogColorMap CatalogsDB::DBManager::get_catalog_colors(const int id)
1190 {
1191  return get_catalog_colors()[id]; // good enough for now
1192 };
1193 
1194 std::pair<bool, QString>
1195 CatalogsDB::DBManager::insert_catalog_colors(const int id, const CatalogColorMap &colors)
1196 {
1197  QMutexLocker _{ &m_mutex };
1198 
1199  QSqlQuery query{ m_db };
1200 
1201  if (!query.prepare(SqlStatements::insert_color))
1202  {
1203  return { false, query.lastError().text() };
1204  }
1205 
1206  query.bindValue(":catalog", id);
1207  for (const auto &item : colors)
1208  {
1209  query.bindValue(":scheme", item.first);
1210  query.bindValue(":color", item.second);
1211 
1212  if (!query.exec())
1213  return { false, query.lastError().text() };
1214  }
1215 
1216  return { true, "" };
1217 };
T & first()
QString maintainer
The catalog maintainer.
Definition: catalogsdb.h:101
std::optional< QSqlQuery > query(const QString &queryStatement)
std::pair< bool, QString > import_catalog(const QString &file_path, const bool overwrite=false)
Loads a dumped catalog from path file_path.
Definition: catalogsdb.cpp:800
bool compile_master_catalog()
Compiles the master catalog by merging the individual catalogs based on oid and precedence and create...
Definition: catalogsdb.cpp:337
std::pair< bool, QString > add_object(const int catalog_id, const SkyObject::TYPE t, const CachingDms &r, const CachingDms &d, const QString &n, const float m=NaN::f, const QString &lname=QString(), const QString &catalog_identifier=QString(), const float a=0.0, const float b=0.0, const double pa=0.0, const float flux=0)
Add a CatalogObject to a table with catalog_id.
Definition: catalogsdb.cpp:687
QString author
The author of the catalog.
Definition: catalogsdb.h:59
std::pair< bool, QString > add_objects(const int catalog_id, const CatalogObjectVector &objects)
Add the objects to a table with catalog_id.
Type type(const QSqlDatabase &db)
int id
The catalog id.
Definition: catalogsdb.h:41
QDateTime currentDateTime()
Stores dms coordinates for a point in the sky. for converting between coordinate systems.
Definition: skypoint.h:44
QString license
The catalog license.
Definition: catalogsdb.h:96
bool copy(const QString &newName)
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition: cachingdms.h:18
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
ColorMap get_catalog_colors()
QString name() const const
bool rollback()
std::pair< bool, QString > remove_catalog(const int id)
remove a catalog
Definition: catalogsdb.cpp:879
virtual QString name(void) const
Definition: skyobject.h:145
float mag() const
Definition: skyobject.h:206
bool enabled
Wether the catalog is enabled.
Definition: catalogsdb.h:80
int find_suitable_catalog_id()
Finds the smallest free id for a catalog.
Definition: catalogsdb.cpp:953
Manages the catalog database and provides an interface to provide an interface to query and modify th...
Definition: catalogsdb.h:182
QList::const_iterator constBegin() const const
const std::pair< bool, Catalog > get_catalog(const int id)
Definition: catalogsdb.cpp:378
CatalogObjectList find_objects_by_wildcard(const QString &wildcard, const int limit=-1)
Find an objects by searching the name four wildcard.
bool update_catalog_views()
Updates the all_catalog_view so that it includes all known catalogs.
Definition: catalogsdb.cpp:242
std::pair< bool, QString > set_catalog_enabled(const int id, const bool enabled)
Enable or disable a catalog.
Definition: catalogsdb.cpp:602
std::pair< bool, QString > insert_catalog_colors(const int id, const CatalogColorMap &colors)
Saves the configures colors of the catalog with id id in colors into the database.
std::pair< bool, QString > remove_object(const int catalog_id, const CatalogObject::oid &id)
Remove the catalog object with the oid from the catalog with the catalog_id.
Definition: catalogsdb.cpp:723
QString color
The catalog color in the form [default color];[scheme file name];[color]....
Definition: catalogsdb.h:91
bool exec(const QString &query)
DBManager(const QString &filename)
Constructs a database manager from the filename which is resolved to a path in the kstars data direct...
Definition: catalogsdb.cpp:81
void resize(int size)
const std::pair< bool, CatalogStatistics > get_master_statistics()
const std::vector< Catalog > get_catalogs(bool include_disabled=false)
Definition: catalogsdb.cpp:621
int type(void) const
Definition: skyobject.h:188
std::pair< bool, QString > register_catalog(const int id, const QString &name, const bool mut, const bool enabled, const double precedence, const QString &author=cat_defaults.author, const QString &source=cat_defaults.source, const QString &description=cat_defaults.description, const int version=cat_defaults.version, const QString &color=cat_defaults.color, const QString &license=cat_defaults.license, const QString &maintainer=cat_defaults.maintainer, const QDateTime &timestamp=cat_defaults.timestamp)
Registers a new catalog in the database.
Definition: catalogsdb.cpp:309
CatalogObjectList get_objects(float maglim=default_maglim, int limit=-1)
Get limit objects with magnitude smaller than maglim (smaller = brighter) from the database.
Definition: catalogsdb.cpp:562
float a() const
QSqlDatabase addDatabase(const QString &type, const QString &connectionName)
QString i18n(const char *text, const TYPE &arg...)
CatalogObjectList find_objects_by_name(const QString &name, const int limit=-1, const bool exactMatchOnly=false)
Find an objects by name.
Definition: catalogsdb.cpp:490
QString source
The catalog source.
Definition: catalogsdb.h:64
void setDatabaseName(const QString &name)
std::pair< bool, QString > update_catalog_meta(const Catalog &cat)
Update the metatadata catalog.
Definition: catalogsdb.cpp:932
void bindValue(const QString &placeholder, const QVariant &val, QSql::ParamType paramType)
int length() const const
CatalogObjectList get_objects_all()
Get all objects from the database.
Definition: catalogsdb.cpp:571
double precedence
The precedence level of a catalog.
Definition: catalogsdb.h:54
void finish()
double pa() const override
bool next()
QTextStream & dec(QTextStream &stream)
std::pair< bool, QString > dump_catalog(int catalog_id, QString file_path)
Dumps the catalog with id into the file under the path file_path.
Definition: catalogsdb.cpp:738
const oid getId() const
Holds statistical information about the objects in a catalog.
Definition: catalogsdb.h:118
QString join(const QString &separator) const const
bool mut
Wether the catalog is mutable.
Definition: catalogsdb.h:75
int version
The catalog version.
Definition: catalogsdb.h:85
QString name
The catalog mame.
Definition: catalogsdb.h:46
An angle, stored as degrees, but expressible in many ways.
Definition: dms.h:37
std::pair< bool, CatalogObject > get_object(const CatalogObject::oid &oid)
Get an object by oid.
Definition: catalogsdb.cpp:538
QSqlError lastError() const const
QString text() const const
float b() const
QString filePath(const QString &fileName) const const
float flux() const
QList::iterator erase(QList::iterator pos)
QDateTime timestamp
Build time of the catalog.
Definition: catalogsdb.h:110
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
void init(KXmlGuiWindow *window, KGameDifficulty *difficulty=nullptr)
const CachingDms & dec0() const
Definition: skypoint.h:257
const double & Degrees() const
Definition: dms.h:141
const char * name(StandardAction id)
QString filePath(const QString &fileName) const const
bool isActive() const const
const CachingDms & ra0() const
Definition: skypoint.h:251
bool isValid() const const
QString description
A (short) description for the catalog.
Definition: catalogsdb.h:70
bool transaction()
const std::pair< bool, CatalogStatistics > get_catalog_statistics(const int catalog_id)
virtual QString longname(void) const
Definition: skyobject.h:164
bool catalog_exists(const int id)
Definition: catalogsdb.cpp:395
const oid getObjectId() const
CatalogObjectList get_objects_in_catalog(SkyObject::TYPE type, const int catalog_id, float maglim=default_maglim, int limit=-1)
Get limit objects from the catalog with catalog_id of type with magnitude smaller than maglim (smalle...
Definition: catalogsdb.cpp:589
Trixel index(const SkyPoint *p)
returns the index of the trixel containing p.
Definition: skymesh.cpp:74
QSqlError lastError() const const
QSqlQuery exec(const QString &query) const const
const QString & catalogIdentifier() const
A simple container object to hold the minimum information for a Deep Sky Object to be drawn on the sk...
Definition: catalogobject.h:40
bool prepare(const QString &query)
A simple struct to hold information about catalogs.
Definition: catalogsdb.h:36
static SkyMesh * Create(int level)
creates the single instance of SkyMesh.
Definition: skymesh.cpp:25
std::tuple< bool, const QString, CatalogObjectList > general_master_query(const QString &where, const QString &order_by="", const int limit=-1)
Find an objects by searching the master catlog with a query like SELECT ...
std::pair< bool, QString > copy_objects(const int id_1, const int id_2)
Clone objects from the catalog with id_1 to another with id_2.
Definition: catalogsdb.cpp:913
char * toString(const EngineQuery &query)
Database related error, thrown when database access fails or an action does not succeed.
Definition: catalogsdb.h:687
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 04:06:15 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.