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

KDE's Doxygen guidelines are available online.