MythTV master
mythdbcon.cpp
Go to the documentation of this file.
1#include <unistd.h>
2
3// ANSI C
4#include <cstdlib>
5#include <thread>
6
7// Qt
8#include <QCoreApplication>
9#include <QElapsedTimer>
10#include <QRegularExpression>
11#include <QSemaphore>
12#include <QSqlDriver>
13#include <QSqlError>
14#include <QSqlField>
15#include <QSqlRecord>
16#include <QVector>
17#include <utility>
18
19// MythTV
20#include "compat.h"
21#include "mythdbcon.h"
22#include "mythdb.h"
23#include "mythcorecontext.h"
24#include "mythlogging.h"
25#include "mythsystemlegacy.h"
26#include "exitcodes.h"
27#include "mthread.h"
28#include "mythdate.h"
29#include "portchecker.h"
30#include "mythmiscutil.h"
31#include "mythrandom.h"
32
33#define DEBUG_RECONNECT 0
34#if DEBUG_RECONNECT
35#include <cstdlib>
36#endif
37
38static constexpr std::chrono::seconds kPurgeTimeout { 1h };
39
40static QMutex sMutex;
41
42bool TestDatabase(const QString& dbHostName,
43 const QString& dbUserName,
44 QString dbPassword,
45 QString dbName,
46 int dbPort)
47{
48 // ensure only one of these runs at a time, otherwise
49 // a segfault may happen as a connection is destroyed while
50 // being used. QSqlDatabase will remove a connection
51 // if another is created with the same name.
52 QMutexLocker locker(&sMutex);
53 bool ret = false;
54
55 if (dbHostName.isEmpty() || dbUserName.isEmpty())
56 return ret;
57
58 auto *db = new MSqlDatabase("dbtest");
59 if (!db)
60 return ret;
61
62 DatabaseParams dbparms;
63 dbparms.m_dbName = std::move(dbName);
64 dbparms.m_dbUserName = dbUserName;
65 dbparms.m_dbPassword = std::move(dbPassword);
66 dbparms.m_dbHostName = dbHostName;
67 dbparms.m_dbPort = dbPort;
68
69 // Just use some sane defaults for these values
70 dbparms.m_wolEnabled = false;
71 dbparms.m_wolReconnect = 1s;
72 dbparms.m_wolRetry = 3;
73 dbparms.m_wolCommand = QString();
74
75 db->SetDBParams(dbparms);
76
77 ret = db->OpenDatabase(true);
78
79 delete db;
80 db = nullptr;
81
82 return ret;
83}
84
85MSqlDatabase::MSqlDatabase(QString name, QString driver)
86 : m_name(std::move(name)), m_driver(std::move(driver))
87{
88 if (!QSqlDatabase::isDriverAvailable(m_driver))
89 {
90 LOG(VB_FLUSH, LOG_CRIT,
91 QString("FATAL: Unable to load the QT %1 driver, is it installed?")
92 .arg(m_driver));
93 exit(GENERIC_EXIT_DB_ERROR); // Exits before we can process the log queue
94 //return;
95 }
96
97 m_db = QSqlDatabase::addDatabase(m_driver, m_name);
98 LOG(VB_DATABASE, LOG_INFO, "Database object created: " + m_name);
99
100 if (!m_db.isValid() || m_db.isOpenError())
101 {
102 LOG(VB_FLUSH, LOG_CRIT, MythDB::DBErrorMessage(m_db.lastError()));
103 LOG(VB_FLUSH, LOG_CRIT, QString("FATAL: Unable to create database object (%1), the installed QT driver may be invalid.").arg(m_name));
104 exit(GENERIC_EXIT_DB_ERROR); // Exits before we can process the log queue
105 //return;
106 }
107 m_lastDBKick = MythDate::current().addSecs(-60);
108}
109
111{
112 if (m_db.isOpen())
113 {
114 m_db.close();
115 m_db = QSqlDatabase(); // forces a destroy and must be done before
116 // removeDatabase() so that connections
117 // and queries are cleaned up correctly
118 QSqlDatabase::removeDatabase(m_name);
119 LOG(VB_DATABASE, LOG_INFO, "Database object deleted: " + m_name);
120 }
121}
122
124{
125 if (m_db.isValid())
126 {
127 if (m_db.isOpen())
128 return true;
129 }
130 return false;
131}
132
134{
135 if (gCoreContext->GetDB()->IsDatabaseIgnored() && m_name != "dbtest")
136 return false;
137 if (!m_db.isValid())
138 {
139 LOG(VB_GENERAL, LOG_ERR,
140 "MSqlDatabase::OpenDatabase(), db object is not valid!");
141 return false;
142 }
143
144 bool connected = true;
145
146 if (!m_db.isOpen())
147 {
148 if (!skipdb)
149 m_dbparms = GetMythDB()->GetDatabaseParams();
150 m_db.setDatabaseName(m_dbparms.m_dbName);
151 m_db.setUserName(m_dbparms.m_dbUserName);
152 m_db.setPassword(m_dbparms.m_dbPassword);
153
154 if (m_dbparms.m_dbHostName.isEmpty()) // Bootstrapping without a database?
155 {
156 // Pretend to be connected to reduce errors
157 return true;
158 }
159
160 // code to ensure that a link-local ip address has the scope
161 int port = 3306;
163 port = m_dbparms.m_dbPort;
165 m_db.setHostName(m_dbparms.m_dbHostName);
166
168 m_db.setPort(m_dbparms.m_dbPort);
169
170 // Prefer using the faster localhost connection if using standard
171 // ports, even if the user specified a DBHostName of 127.0.0.1. This
172 // will cause MySQL to use a Unix socket (on *nix) or shared memory (on
173 // Windows) connection.
174 if ((m_dbparms.m_dbPort == 0 || m_dbparms.m_dbPort == 3306) &&
175 m_dbparms.m_dbHostName == "127.0.0.1")
176 m_db.setHostName("localhost");
177
178 // Default read timeout is 10 mins - set a better value 300 seconds
179 if (m_dbparms.m_dbType != "QSQLITE")
180 m_db.setConnectOptions(QString("MYSQL_OPT_READ_TIMEOUT=300"));
181
182 connected = m_db.open();
183
184 if (!connected && m_dbparms.m_wolEnabled
186 {
187 int trycount = 0;
188
189 while (!connected && trycount++ < m_dbparms.m_wolRetry)
190 {
191 LOG(VB_GENERAL, LOG_INFO,
192 QString("Using WOL to wakeup database server (Try %1 of "
193 "%2)")
194 .arg(trycount).arg(m_dbparms.m_wolRetry));
195
197 {
198 LOG(VB_GENERAL, LOG_ERR,
199 QString("Failed to run WOL command '%1'")
200 .arg(m_dbparms.m_wolCommand));
201 }
202
203 std::this_thread::sleep_for(m_dbparms.m_wolReconnect);
204 connected = m_db.open();
205 }
206
207 if (!connected)
208 {
209 LOG(VB_GENERAL, LOG_ERR,
210 "WOL failed, unable to connect to database!");
211 }
212 }
213 if (connected)
214 {
215 LOG(VB_DATABASE, LOG_INFO,
216 QString("[%1] Connected to database '%2' at host: %3")
217 .arg(m_name, m_db.databaseName(), m_db.hostName()));
218
220
221 // WriteDelayed depends on SetHaveDBConnection() and SetHaveSchema()
222 // both being called with true, so order is important here.
223 GetMythDB()->SetHaveDBConnection(true);
224 if (!GetMythDB()->HaveSchema())
225 {
226 // We can't just check the count of QSqlDatabase::tables()
227 // because it returns all tables visible to the user in *all*
228 // databases (not just the current DB).
229 bool have_schema = false;
230 QString sql = "SELECT COUNT(TABLE_NAME) "
231 " FROM INFORMATION_SCHEMA.TABLES "
232 " WHERE TABLE_SCHEMA = DATABASE() "
233 " AND TABLE_TYPE = 'BASE TABLE';";
234 // We can't use MSqlQuery to determine if we have a schema,
235 // since it will open a new connection, which will try to check
236 // if we have a schema
237 QSqlQuery query(sql, m_db); // don't convert to MSqlQuery
238 if (query.next())
239 have_schema = query.value(0).toInt() > 1;
240 GetMythDB()->SetHaveSchema(have_schema);
241 }
242 GetMythDB()->WriteDelayedSettings();
243 }
244 }
245
246 if (!connected)
247 {
248 GetMythDB()->SetHaveDBConnection(false);
249 LOG(VB_GENERAL, LOG_ERR, QString("[%1] Unable to connect to database!").arg(m_name));
250 LOG(VB_GENERAL, LOG_ERR, MythDB::DBErrorMessage(m_db.lastError()));
251 }
252
253 return connected;
254}
255
257{
258 m_lastDBKick = MythDate::current().addSecs(-60);
259
260 if (!m_db.isOpen())
261 m_db.open();
262
263 return m_db.isOpen();
264}
265
267{
268 m_db.close();
269 m_db.open();
270
271 bool open = m_db.isOpen();
272 if (open)
273 {
274 LOG(VB_GENERAL, LOG_INFO, "MySQL reconnected successfully");
276 }
277
278 return open;
279}
280
282{
283 QSqlQuery query(m_db);
284
285 // Make sure NOW() returns time in UTC...
286 query.exec("SET @@session.time_zone='+00:00'");
287 // Disable strict mode
288 query.exec("SET @@session.sql_mode=''");
289}
290
291// -----------------------------------------------------------------------
292
293
294
296{
298
299 if (m_connCount != 0 || m_schedCon || m_channelCon)
300 {
301 LOG(VB_GENERAL, LOG_CRIT,
302 "MDBManager exiting with connections still open");
303 }
304#if 0 /* some post logStop() debugging... */
305 cout<<"m_connCount: "<<m_connCount<<endl;
306 cout<<"m_schedCon: "<<m_schedCon<<endl;
307 cout<<"m_channelCon: "<<m_channelCon<<endl;
308#endif
309}
310
312{
314
315 m_lock.lock();
316
317 MSqlDatabase *db = nullptr;
318
319#if REUSE_CONNECTION
320 if (reuse)
321 {
322 db = m_inuse[QThread::currentThread()];
323 if (db != nullptr)
324 {
325 m_inuseCount[QThread::currentThread()]++;
326 m_lock.unlock();
327 return db;
328 }
329 }
330#endif
331
332 DBList &list = m_pool[QThread::currentThread()];
333 if (list.isEmpty())
334 {
335 DatabaseParams params = GetMythDB()->GetDatabaseParams();
336 db = new MSqlDatabase("DBManager" + QString::number(m_nextConnID++),
337 params.m_dbType);
338 ++m_connCount;
339 LOG(VB_DATABASE, LOG_INFO,
340 QString("New DB connection, total: %1").arg(m_connCount));
341 }
342 else
343 {
344 db = list.back();
345 list.pop_back();
346 }
347
348#if REUSE_CONNECTION
349 if (reuse)
350 {
351 m_inuseCount[QThread::currentThread()]=1;
352 m_inuse[QThread::currentThread()] = db;
353 }
354#endif
355
356 m_lock.unlock();
357
358 db->OpenDatabase();
359
360 return db;
361}
362
364{
365 m_lock.lock();
366
367#if REUSE_CONNECTION
368 if (db == m_inuse[QThread::currentThread()])
369 {
370 int cnt = --m_inuseCount[QThread::currentThread()];
371 if (cnt > 0)
372 {
373 m_lock.unlock();
374 return;
375 }
376 m_inuse[QThread::currentThread()] = nullptr;
377 }
378#endif
379
380 if (db)
381 {
383 m_pool[QThread::currentThread()].push_front(db);
384 }
385
386 m_lock.unlock();
387
389}
390
392{
393 QMutexLocker locker(&m_lock);
394
395 leaveOne = leaveOne || (gCoreContext && gCoreContext->IsUIThread());
396
397 QDateTime now = MythDate::current();
398 DBList &list = m_pool[QThread::currentThread()];
399 DBList::iterator it = list.begin();
400
401 uint purgedConnections = 0;
402 uint totalConnections = 0;
403 MSqlDatabase *newDb = nullptr;
404 while (it != list.end())
405 {
406 totalConnections++;
407 if ((*it)->m_lastDBKick.secsTo(now) <= kPurgeTimeout.count())
408 {
409 ++it;
410 continue;
411 }
412
413 // This connection has not been used in the kPurgeTimeout
414 // seconds close it.
415 MSqlDatabase *entry = *it;
416 it = list.erase(it);
417 --m_connCount;
418 purgedConnections++;
419
420 // Qt's MySQL driver apparently keeps track of the number of
421 // open DB connections, and when it hits 0, calls
422 // my_thread_global_end(). The mysql library then assumes the
423 // application is ending and that all threads that created DB
424 // connections have already exited. This is rarely true, and
425 // may result in the mysql library pausing 5 seconds and
426 // printing a message like "Error in my_thread_global_end(): 1
427 // threads didn't exit". This workaround simply creates an
428 // extra DB connection before all pooled connections are
429 // purged so that my_thread_global_end() won't be called.
430 if (leaveOne && it == list.end() &&
431 purgedConnections > 0 &&
432 totalConnections == purgedConnections)
433 {
434 newDb = new MSqlDatabase("DBManager" +
435 QString::number(m_nextConnID++));
436 ++m_connCount;
437 LOG(VB_GENERAL, LOG_INFO,
438 QString("New DB connection, total: %1").arg(m_connCount));
440 }
441
442 LOG(VB_DATABASE, LOG_INFO, "Deleting idle DB connection...");
443 delete entry;
444 LOG(VB_DATABASE, LOG_INFO, "Done deleting idle DB connection.");
445 }
446 if (newDb)
447 list.push_front(newDb);
448
449 if (purgedConnections)
450 {
451 LOG(VB_DATABASE, LOG_INFO,
452 QString("Purged %1 idle of %2 total DB connections.")
453 .arg(purgedConnections).arg(totalConnections));
454 }
455}
456
458{
459 if (!dbcon)
460 return nullptr;
461
462 if (!*dbcon)
463 {
464 *dbcon = new MSqlDatabase(name);
465 LOG(VB_GENERAL, LOG_INFO, "New static DB connection" + name);
466 }
467
468 (*dbcon)->OpenDatabase();
469
470 if (!m_staticPool[QThread::currentThread()].contains(*dbcon))
471 m_staticPool[QThread::currentThread()].push_back(*dbcon);
472
473 return *dbcon;
474}
475
477{
478 return getStaticCon(&m_schedCon, "SchedCon");
479}
480
482{
483 return getStaticCon(&m_channelCon, "ChannelCon");
484}
485
487{
488 m_lock.lock();
489 DBList list = m_pool[QThread::currentThread()];
490 m_pool[QThread::currentThread()].clear();
491 m_lock.unlock();
492
493 for (auto *conn : std::as_const(list))
494 {
495 LOG(VB_DATABASE, LOG_INFO,
496 "Closing DB connection named '" + conn->m_name + "'");
497 conn->m_db.close();
498 delete conn;
499 m_connCount--;
500 }
501
502 m_lock.lock();
503 DBList &slist = m_staticPool[QThread::currentThread()];
504 while (!slist.isEmpty())
505 {
506 MSqlDatabase *db = slist.takeFirst();
507 LOG(VB_DATABASE, LOG_INFO,
508 "Closing DB connection named '" + db->m_name + "'");
509 db->m_db.close();
510 delete db;
511
512 if (db == m_schedCon)
513 m_schedCon = nullptr;
514 if (db == m_channelCon)
515 m_channelCon = nullptr;
516 }
517 m_lock.unlock();
518}
519
520
521// -----------------------------------------------------------------------
522
524{
525 qi.db = nullptr;
526 qi.qsqldb = QSqlDatabase();
527 qi.returnConnection = true;
528}
529
530
532 : QSqlQuery(QString(), qi.qsqldb),
533 m_db(qi.db),
534 m_isConnected(m_db && m_db->isOpen()),
535 m_returnConnection(qi.returnConnection)
536{
537}
538
540{
542 {
543 MDBManager *dbmanager = GetMythDB()->GetDBManager();
544
545 if (dbmanager && m_db)
546 {
547 dbmanager->pushConnection(m_db);
548 }
549 }
550}
551
553{
554 bool reuse = kNormalConnection == _reuse;
555 MSqlDatabase *db = GetMythDB()->GetDBManager()->popConnection(reuse);
556 MSqlQueryInfo qi;
557
559
560
561 // Bootstrapping without a database?
562 //if (db->pretendHaveDB)
563 if (db->m_db.hostName().isEmpty())
564 {
565 // Return an invalid database so that QSqlQuery does nothing.
566 // Also works around a Qt4 bug where QSqlQuery::~QSqlQuery
567 // calls QMYSQLResult::cleanup() which uses mysql_next_result()
568
569 GetMythDB()->GetDBManager()->pushConnection(db);
570 qi.returnConnection = false;
571 return qi;
572 }
573
574 qi.db = db;
575 qi.qsqldb = db->db();
576
577 db->KickDatabase();
578
579 return qi;
580}
581
583{
584 MSqlDatabase *db = GetMythDB()->GetDBManager()->getSchedCon();
585 MSqlQueryInfo qi;
586
588 qi.returnConnection = false;
589
590 if (db)
591 {
592 qi.db = db;
593 qi.qsqldb = db->db();
594
595 db->KickDatabase();
596 }
597
598 return qi;
599}
600
602{
603 MSqlDatabase *db = GetMythDB()->GetDBManager()->getChannelCon();
604 MSqlQueryInfo qi;
605
607 qi.returnConnection = false;
608
609 if (db)
610 {
611 qi.db = db;
612 qi.qsqldb = db->db();
613
614 db->KickDatabase();
615 }
616
617 return qi;
618}
619
621{
622 if (!m_db)
623 {
624 // Database structure's been deleted
625 return false;
626 }
627
628 if (m_lastPreparedQuery.isEmpty())
629 {
630 LOG(VB_GENERAL, LOG_ERR,
631 "MSqlQuery::exec(void) called without a prepared query.");
632 return false;
633 }
634
635#if DEBUG_RECONNECT
636 if (rand_bool(50))
637 {
638 LOG(VB_GENERAL, LOG_INFO,
639 "MSqlQuery disconnecting DB to test reconnection logic");
640 m_db->m_db.close();
641 }
642#endif
643
644 // Database connection down. Try to restart it, give up if it's still
645 // down
646 if (!m_db->isOpen() && !Reconnect())
647 {
648 LOG(VB_GENERAL, LOG_INFO, "MySQL server disconnected");
649 return false;
650 }
651
652 QElapsedTimer timer;
653 timer.start();
654
655 bool result = QSqlQuery::exec();
656 qint64 elapsed = timer.elapsed();
657
658 if (!result && lostConnectionCheck())
659 result = QSqlQuery::exec();
660
661 if (!result)
662 {
663 QString err = MythDB::GetError("MSqlQuery", *this);
664#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
665 MSqlBindings tmp = QSqlQuery::boundValues();
666#else
667 QVariantList tmp = QSqlQuery::boundValues();
668#endif
669 bool has_null_strings = false;
670 // NOLINTNEXTLINE(modernize-loop-convert)
671 for (auto it = tmp.begin(); it != tmp.end(); ++it)
672 {
673#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
674 auto type = static_cast<QMetaType::Type>(it->type());
675#else
676 auto type = it->typeId();
677#endif
678 if (type != QMetaType::QString)
679 continue;
680 if (it->isNull() || it->toString().isNull())
681 {
682 has_null_strings = true;
683 *it = QVariant(QString(""));
684 }
685 }
686 if (has_null_strings)
687 {
688#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
689 bindValues(tmp);
690#else
691 for (int i = 0; i < static_cast<int>(tmp.size()); i++)
692 QSqlQuery::bindValue(i, tmp.at(i));
693#endif
694 timer.restart();
695 result = QSqlQuery::exec();
696 elapsed = timer.elapsed();
697 }
698 if (result)
699 {
700 LOG(VB_GENERAL, LOG_ERR,
701 QString("Original query failed, but resend with empty "
702 "strings in place of NULL strings worked. ") +
703 "\n" + err);
704 }
705 }
706
707 if (VERBOSE_LEVEL_CHECK(VB_DATABASE, LOG_INFO))
708 {
709 QString str = lastQuery();
710
711 // Sadly, neither executedQuery() nor lastQuery() display
712 // the values in bound queries against a MySQL5 database.
713 // So, replace the named placeholders with their values.
714
715#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
716 QMapIterator<QString, QVariant> b = boundValues();
717 while (b.hasNext())
718 {
719 b.next();
720 str.replace(b.key(), '\'' + b.value().toString() + '\'');
721 }
722#else
723 QVariantList b = boundValues();
724 static const QRegularExpression placeholders { "(:\\w+)" };
725 auto match = placeholders.match(str);
726 while (match.hasMatch())
727 {
728 str.replace(match.capturedStart(), match.capturedLength(),
729 b.isEmpty()
730 ? "\'INVALID\'"
731 : '\'' + b.takeFirst().toString() + '\'');
732 match = placeholders.match(str);
733 }
734#endif
735
736 LOG(VB_DATABASE, LOG_INFO,
737 QString("MSqlQuery::exec(%1) %2%3%4")
738 .arg(m_db->MSqlDatabase::GetConnectionName(), str,
739 QString(" <<<< Took %1ms").arg(QString::number(elapsed)),
740 isSelect()
741 ? QString(", Returned %1 row(s)").arg(size())
742 : QString()));
743 }
744
745 return result;
746}
747
748bool MSqlQuery::exec(const QString &query)
749{
750 if (!m_db)
751 {
752 // Database structure's been deleted
753 return false;
754 }
755
756 // Database connection down. Try to restart it, give up if it's still
757 // down
758 if (!m_db->isOpen() && !Reconnect())
759 {
760 LOG(VB_GENERAL, LOG_INFO, "MySQL server disconnected");
761 return false;
762 }
763
764 bool result = QSqlQuery::exec(query);
765
766 if (!result && lostConnectionCheck())
767 result = QSqlQuery::exec(query);
768
769 LOG(VB_DATABASE, LOG_INFO,
770 QString("MSqlQuery::exec(%1) %2%3")
771 .arg(m_db->MSqlDatabase::GetConnectionName(), query,
772 isSelect()
773 ? QString(" <<<< Returns %1 row(s)").arg(size())
774 : QString()));
775
776 return result;
777}
778
779bool MSqlQuery::seekDebug(const char *type, bool result,
780 int where, bool relative) const
781{
782 if (result && VERBOSE_LEVEL_CHECK(VB_DATABASE, LOG_DEBUG))
783 {
784 QString str;
785 QSqlRecord rec = record();
786
787 for (int i = 0; i < rec.count(); i++)
788 {
789 if (!str.isEmpty())
790 str.append(", ");
791
792 str.append(rec.fieldName(i) + " = " +
793 value(i).toString());
794 }
795
796 if (QString("seek")==type)
797 {
798 LOG(VB_DATABASE, LOG_DEBUG,
799 QString("MSqlQuery::seek(%1,%2,%3) Result: \"%4\"")
800 .arg(m_db->MSqlDatabase::GetConnectionName())
801 .arg(where).arg(relative)
802 .arg(str));
803 }
804 else
805 {
806 LOG(VB_DATABASE, LOG_DEBUG,
807 QString("MSqlQuery::%1(%2) Result: \"%3\"")
808 .arg(type, m_db->MSqlDatabase::GetConnectionName(), str));
809 }
810 }
811 return result;
812}
813
815{
816 return seekDebug("next", QSqlQuery::next(), 0, false);
817}
818
820{
821 return seekDebug("previous", QSqlQuery::previous(), 0, false);
822}
823
825{
826 return seekDebug("first", QSqlQuery::first(), 0, false);
827}
828
830{
831 return seekDebug("last", QSqlQuery::last(), 0, false);
832}
833
834bool MSqlQuery::seek(int where, bool relative)
835{
836 return seekDebug("seek", QSqlQuery::seek(where, relative), where, relative);
837}
838
839bool MSqlQuery::prepare(const QString& query)
840{
841 if (!m_db)
842 {
843 // Database structure's been deleted
844 return false;
845 }
846
847 m_lastPreparedQuery = query;
848
849 if (!m_db->isOpen() && !Reconnect())
850 {
851 LOG(VB_GENERAL, LOG_INFO, "MySQL server disconnected");
852 return false;
853 }
854
855 // QT docs indicate that there are significant speed ups and a reduction
856 // in memory usage by enabling forward-only cursors
857 //
858 // Unconditionally enable this since all existing uses of the database
859 // iterate forward over the result set.
860 setForwardOnly(true);
861
862 bool ok = QSqlQuery::prepare(query);
863
864 if (!ok && lostConnectionCheck())
865 ok = true;
866
867 if (!ok && !(GetMythDB()->SuppressDBMessages()))
868 {
869 LOG(VB_GENERAL, LOG_ERR,
870 QString("Error preparing query: %1").arg(query));
871 LOG(VB_GENERAL, LOG_ERR,
872 MythDB::DBErrorMessage(QSqlQuery::lastError()));
873 }
874
875 return ok;
876}
877
879{
880 MSqlDatabase *db = GetMythDB()->GetDBManager()->popConnection(true);
881
882 // popConnection() has already called OpenDatabase(),
883 // so we only have to check if it was successful:
884 bool isOpen = db->isOpen();
885
886 GetMythDB()->GetDBManager()->pushConnection(db);
887 return isOpen;
888}
889
890void MSqlQuery::bindValue(const QString &placeholder, const QVariant &val)
891{
892#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
893 if (static_cast<QMetaType::Type>(val.type()) == QMetaType::QDateTime)
894 {
895 QSqlQuery::bindValue(placeholder,
896 MythDate::toString(val.toDateTime(), MythDate::kDatabase),
897 QSql::In);
898 return;
899 }
900#endif
901 QSqlQuery::bindValue(placeholder, val, QSql::In);
902}
903
904void MSqlQuery::bindValueNoNull(const QString &placeholder, const QVariant &val)
905{
906#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
907 auto type = static_cast<QMetaType::Type>(val.type());
908#else
909 auto type = val.typeId();
910#endif
911 if (type == QMetaType::QString && val.toString().isNull())
912 {
913 QSqlQuery::bindValue(placeholder, QString(""), QSql::In);
914 return;
915 }
916#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
917 if (type == QMetaType::QDateTime)
918 {
919 QSqlQuery::bindValue(placeholder,
920 MythDate::toString(val.toDateTime(), MythDate::kDatabase),
921 QSql::In);
922 return;
923 }
924#endif
925 QSqlQuery::bindValue(placeholder, val, QSql::In);
926}
927
929{
930 MSqlBindings::const_iterator it;
931 for (it = bindings.begin(); it != bindings.end(); ++it)
932 {
933 bindValue(it.key(), it.value());
934 }
935}
936
938{
939 return QSqlQuery::lastInsertId();
940}
941
943{
944 if (!m_db->Reconnect())
945 return false;
946 if (!m_lastPreparedQuery.isEmpty())
947 {
948#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
949 MSqlBindings tmp = QSqlQuery::boundValues();
950 if (!QSqlQuery::prepare(m_lastPreparedQuery))
951 return false;
952 bindValues(tmp);
953#else
954 QVariantList tmp = QSqlQuery::boundValues();
955 if (!QSqlQuery::prepare(m_lastPreparedQuery))
956 return false;
957 for (int i = 0; i < static_cast<int>(tmp.size()); i++)
958 QSqlQuery::bindValue(i, tmp.at(i));
959#endif
960 }
961 return true;
962}
963
965{
966 // MySQL: Error number: 2006; Symbol: CR_SERVER_GONE_ERROR
967 // MySQL: Error number: 2013; Symbol: CR_SERVER_LOST
968 // MySQL: Error number: 4031; Symbol: ER_CLIENT_INTERACTION_TIMEOUT
969 // Note: In MariaDB, 4031 = ER_REFERENCED_TRG_DOES_NOT_EXIST
970
971 static QStringList kLostConnectionCodes = { "2006", "2013", "4031" };
972
973 QString error_code = QSqlQuery::lastError().nativeErrorCode();
974
975 // Make capturing of new 'lost connection' like error codes easy.
976 LOG(VB_GENERAL, LOG_DEBUG, QString("SQL Native Error Code: %1")
977 .arg(error_code));
978
979 // If the query failed with any of the error codes that say the server
980 // is gone, close and reopen the database connection.
981 return (kLostConnectionCodes.contains(error_code) && Reconnect());
982
983}
984
986{
987 MSqlBindings::Iterator it;
988 for (it = addfrom.begin(); it != addfrom.end(); ++it)
989 {
990 output.insert(it.key(), it.value());
991 }
992}
993
994struct Holder {
995 explicit Holder( QString hldr = QString(), int pos = -1 )
996 : m_holderName(std::move( hldr )), m_holderPos( pos ) {}
997
998 bool operator==( const Holder& h ) const
999 { return h.m_holderPos == m_holderPos && h.m_holderName == m_holderName; }
1000 bool operator!=( const Holder& h ) const
1001 { return h.m_holderPos != m_holderPos || h.m_holderName != m_holderName; }
1004};
1005
1006void MSqlEscapeAsAQuery(QString &query, const MSqlBindings &bindings)
1007{
1008 MSqlQuery result(MSqlQuery::InitCon());
1009
1010 static const QRegularExpression rx { "('[^']+'|:\\w+)",
1011 QRegularExpression::UseUnicodePropertiesOption};
1012
1013 QVector<Holder> holders;
1014
1015 auto matchIter = rx.globalMatch(query);
1016 while (matchIter.hasNext())
1017 {
1018 auto match = matchIter.next();
1019 if (match.capturedLength(1) > 0)
1020 holders.append(Holder(match.captured(), match.capturedStart()));
1021 }
1022
1023 QVariant val;
1024 QString holder;
1025
1026 for (int i = holders.count() - 1; i >= 0; --i)
1027 {
1028 holder = holders[(uint)i].m_holderName;
1029 val = bindings[holder];
1030#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1031 QSqlField f("", val.type());
1032#else
1033 QSqlField f("", val.metaType());
1034#endif
1035 if (val.isNull())
1036 f.clear();
1037 else
1038 f.setValue(val);
1039
1040 query = query.replace((uint)holders[(uint)i].m_holderPos, holder.length(),
1041 result.driver()->formatValue(f));
1042 }
1043}
Structure containing the basic Database parameters.
Definition: mythdbparams.h:11
QString m_dbName
database name
Definition: mythdbparams.h:26
QString m_dbPassword
DB password.
Definition: mythdbparams.h:25
std::chrono::seconds m_wolReconnect
seconds to wait for reconnect
Definition: mythdbparams.h:34
QString m_dbUserName
DB user name.
Definition: mythdbparams.h:24
QString m_dbType
database type (MySQL, Postgres, etc.)
Definition: mythdbparams.h:27
QString m_wolCommand
command to use for wake-on-lan
Definition: mythdbparams.h:36
bool m_wolEnabled
true if wake-on-lan params are used
Definition: mythdbparams.h:33
int m_dbPort
database port
Definition: mythdbparams.h:23
int m_wolRetry
times to retry to reconnect
Definition: mythdbparams.h:35
QString m_dbHostName
database server
Definition: mythdbparams.h:21
DB connection pool, used by MSqlQuery. Do not use directly.
Definition: mythdbcon.h:55
QMutex m_lock
Definition: mythdbcon.h:75
void PurgeIdleConnections(bool leaveOne=false)
Definition: mythdbcon.cpp:391
MSqlDatabase * m_channelCon
Definition: mythdbcon.h:87
void CloseDatabases(void)
Definition: mythdbcon.cpp:486
int m_nextConnID
Definition: mythdbcon.h:83
int m_connCount
Definition: mythdbcon.h:84
void pushConnection(MSqlDatabase *db)
Definition: mythdbcon.cpp:363
QHash< QThread *, int > m_inuseCount
Definition: mythdbcon.h:80
QList< MSqlDatabase * > DBList
Definition: mythdbcon.h:76
MSqlDatabase * getChannelCon(void)
Definition: mythdbcon.cpp:481
~MDBManager(void)
Definition: mythdbcon.cpp:295
QHash< QThread *, DBList > m_staticPool
Definition: mythdbcon.h:88
MSqlDatabase * getStaticCon(MSqlDatabase **dbcon, const QString &name)
Definition: mythdbcon.cpp:457
QHash< QThread *, DBList > m_pool
Definition: mythdbcon.h:77
MSqlDatabase * popConnection(bool reuse)
Definition: mythdbcon.cpp:311
MSqlDatabase * m_schedCon
Definition: mythdbcon.h:86
MSqlDatabase * getSchedCon(void)
Definition: mythdbcon.cpp:476
QHash< QThread *, MSqlDatabase * > m_inuse
Definition: mythdbcon.h:79
QSqlDatabase wrapper, used by MSqlQuery. Do not use directly.
Definition: mythdbcon.h:26
bool OpenDatabase(bool skipdb=false)
Definition: mythdbcon.cpp:133
QDateTime m_lastDBKick
Definition: mythdbcon.h:49
QSqlDatabase m_db
Definition: mythdbcon.h:48
QString m_name
Definition: mythdbcon.h:46
DatabaseParams m_dbparms
Definition: mythdbcon.h:50
bool KickDatabase(void)
Definition: mythdbcon.cpp:256
void InitSessionVars(void)
Definition: mythdbcon.cpp:281
bool Reconnect(void)
Definition: mythdbcon.cpp:266
~MSqlDatabase(void)
Definition: mythdbcon.cpp:110
QString m_driver
Definition: mythdbcon.h:47
QSqlDatabase db(void) const
Definition: mythdbcon.h:41
MSqlDatabase(QString name, QString driver="QMYSQL")
Definition: mythdbcon.cpp:85
bool isOpen(void)
Definition: mythdbcon.cpp:123
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:839
QSqlRecord record(void) const
Definition: mythdbcon.h:216
bool Reconnect(void)
Reconnects server and re-prepares and re-binds the last prepared query.
Definition: mythdbcon.cpp:942
QString m_lastPreparedQuery
Definition: mythdbcon.h:254
bool first(void)
Wrap QSqlQuery::first() so we can display the query results.
Definition: mythdbcon.cpp:824
bool lostConnectionCheck(void)
lostConnectionCheck tests for SQL error codes that indicate the connection to the server has been los...
Definition: mythdbcon.cpp:964
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
static bool testDBConnection()
Checks DB connection + login (login info via Mythcontext)
Definition: mythdbcon.cpp:878
static MSqlQueryInfo SchedCon()
Returns dedicated connection. (Required for using temporary SQL tables.)
Definition: mythdbcon.cpp:582
void bindValues(const MSqlBindings &bindings)
Add all the bindings in the passed in bindings.
Definition: mythdbcon.cpp:928
~MSqlQuery()
Returns connection to pool.
Definition: mythdbcon.cpp:539
@ kNormalConnection
Definition: mythdbcon.h:229
void setForwardOnly(bool f)
Definition: mythdbcon.h:218
bool m_returnConnection
Definition: mythdbcon.h:253
void bindValueNoNull(const QString &placeholder, const QVariant &val)
Add a single binding, taking care not to set a NULL value.
Definition: mythdbcon.cpp:904
QVariantList boundValues(void) const
Definition: mythdbcon.h:211
bool previous(void)
Wrap QSqlQuery::previous() so we can display the query results.
Definition: mythdbcon.cpp:819
bool last(void)
Wrap QSqlQuery::last() so we can display the query results.
Definition: mythdbcon.cpp:829
bool seek(int where, bool relative=false)
Wrap QSqlQuery::seek(int,bool)
Definition: mythdbcon.cpp:834
MSqlQuery(const MSqlQueryInfo &qi)
Get DB connection from pool.
Definition: mythdbcon.cpp:531
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:620
bool seekDebug(const char *type, bool result, int where, bool relative) const
Definition: mythdbcon.cpp:779
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:890
MSqlDatabase * m_db
Definition: mythdbcon.h:251
static MSqlQueryInfo ChannelCon()
Returns dedicated connection. (Required for using temporary SQL tables.)
Definition: mythdbcon.cpp:601
QVariant lastInsertId()
Return the id of the last inserted row.
Definition: mythdbcon.cpp:937
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:814
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:552
const QSqlDriver * driver(void) const
Definition: mythdbcon.h:220
MythDB * GetDB(void)
bool IsWOLAllowed() const
static QString DBErrorMessage(const QSqlError &err)
Definition: mythdb.cpp:230
static QString GetError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:194
Small class to handle TCP port checking and finding link-local context.
Definition: portchecker.h:45
bool resolveLinkLocal(QString &host, int port, std::chrono::milliseconds timeLimit=30s)
Convenience method to resolve link-local address.
unsigned int uint
Definition: compat.h:60
@ GENERIC_EXIT_DB_ERROR
Database error.
Definition: exitcodes.h:20
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythDB * GetMythDB(void)
Definition: mythdb.cpp:50
static void InitMSqlQueryInfo(MSqlQueryInfo &qi)
Definition: mythdbcon.cpp:523
void MSqlEscapeAsAQuery(QString &query, const MSqlBindings &bindings)
Given a partial query string and a bindings object, escape the string.
Definition: mythdbcon.cpp:1006
void MSqlAddMoreBindings(MSqlBindings &output, MSqlBindings &addfrom)
Add the entries in addfrom to the map in output.
Definition: mythdbcon.cpp:985
static QMutex sMutex
Definition: mythdbcon.cpp:40
bool TestDatabase(const QString &dbHostName, const QString &dbUserName, QString dbPassword, QString dbName, int dbPort)
Definition: mythdbcon.cpp:42
static constexpr std::chrono::seconds kPurgeTimeout
Definition: mythdbcon.cpp:38
QMap< QString, QVariant > MSqlBindings
typedef for a map of string -> string bindings for generic queries.
Definition: mythdbcon.h:100
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool MythWakeup(const QString &wakeUpCommand, uint flags, std::chrono::seconds timeout)
Convenience inline random number generator functions.
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kDatabase
Default UTC, database format.
Definition: mythdate.h:27
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
bool rand_bool(uint32_t chance=2)
return a random bool with P(true) = 1/chance
Definition: mythrandom.h:86
QString m_holderName
Definition: mythdbcon.cpp:1002
int m_holderPos
Definition: mythdbcon.cpp:1003
bool operator==(const Holder &h) const
Definition: mythdbcon.cpp:998
bool operator!=(const Holder &h) const
Definition: mythdbcon.cpp:1000
Holder(QString hldr=QString(), int pos=-1)
Definition: mythdbcon.cpp:995
MSqlDatabase Info, used by MSqlQuery. Do not use directly.
Definition: mythdbcon.h:93
bool returnConnection
Definition: mythdbcon.h:96
MSqlDatabase * db
Definition: mythdbcon.h:94
QSqlDatabase qsqldb
Definition: mythdbcon.h:95
#define output