Files
TinyORM/include/orm/databaseconnection.hpp
silverqx 7486b972e0 analyzers suppressed Clang Tidy warning
- updated comment
2024-03-20 14:42:42 +01:00

629 lines
25 KiB
C++

#pragma once
#ifndef ORM_DATABASECONNECTION_HPP
#define ORM_DATABASECONNECTION_HPP
#include "orm/macros/systemheader.hpp"
TINY_SYSTEM_HEADER
#include "orm/concerns/countsqueries.hpp"
#include "orm/concerns/detectslostconnections.hpp"
#include "orm/concerns/logsqueries.hpp"
#include "orm/concerns/managestransactions.hpp"
#include "orm/connectors/connectorinterface.hpp"
#include "orm/exceptions/queryerror.hpp"
#include "orm/query/grammars/grammar.hpp"
#include "orm/query/processors/processor.hpp"
#include "orm/schema/grammars/schemagrammar.hpp"
#include "orm/schema/schemabuilder.hpp"
#include "orm/types/sqlquery.hpp"
TINYORM_BEGIN_COMMON_NAMESPACE
namespace Orm
{
/*! Alias for the QueryGrammar. */
using QueryGrammar = Query::Grammars::Grammar;
/*! Alias for the QueryProcessor. */
using QueryProcessor = Query::Processors::Processor;
/*! Alias for the SchemaBuilder. */
using SchemaBuilder = SchemaNs::SchemaBuilder;
/*! Alias for the SchemaGrammar. */
using SchemaGrammar = SchemaNs::Grammars::SchemaGrammar;
#if defined(__GNUG__) && !defined(__clang__)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
#endif
/*! Database connection base class.
TinyORM's DatabaseConnection never physically connects to the database after
create()/reconnect() method calls, it only creates a connection resolver,
a physical connection to the database is made lazily during a first query call.
It is designed in such a way that you don't have to worry about connecting or
reconnecting to the database, this is handled by the TinyORM library
internally. The reconnection is handled correctly if a connection loss is
detected. */
class SHAREDLIB_EXPORT DatabaseConnection :
public Concerns::DetectsLostConnections,
public Concerns::ManagesTransactions,
public Concerns::LogsQueries,
public Concerns::CountsQueries,
// Needed to suppress the -Wnon-virtual-dtor diagnostic
public std::enable_shared_from_this<DatabaseConnection>
{
Q_DISABLE_COPY_MOVE(DatabaseConnection)
// To access shouldCountElapsed() method
friend Concerns::ManagesTransactions;
/* The friend declaration doesn't affect an ABI or binary compatibility so
wrapping it in the #ifdef is safe:
https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++ */
#ifdef TINYORM_MYSQL_PING
// To access logConnected()/logDisconnected() methods
friend class MySqlConnection;
#endif
protected:
/*! Protected constructor. */
explicit DatabaseConnection(
std::function<Connectors::ConnectionName()> &&connection,
QString &&database = "", QString &&tablePrefix = "",
QtTimeZoneConfig &&qtTimeZone = {QtTimeZoneType::QtTimeSpec, Qt::UTC},
QVariantHash &&config = {});
/*! Protected constructor for SQLite connection. */
explicit DatabaseConnection(
std::function<Connectors::ConnectionName()> &&connection,
QString &&database = "", QString &&tablePrefix = "",
QtTimeZoneConfig &&qtTimeZone = {QtTimeZoneType::QtTimeSpec, Qt::UTC},
std::optional<bool> returnQDateTime = true,
QVariantHash &&config = {});
public:
/*! Pure virtual destructor. */
inline ~DatabaseConnection() override = 0;
/*! Begin a fluent query against a database table. */
std::shared_ptr<QueryBuilder>
table(const QString &table, const QString &as = "");
/*! Get the table prefix for the connection. */
inline QString getTablePrefix() const;
/*! Set the table prefix in use by the connection. */
DatabaseConnection &setTablePrefix(const QString &prefix);
/*! Set the table prefix and return the query grammar. */
BaseGrammar &withTablePrefix(BaseGrammar &grammar) const;
/*! Get a new query builder instance. */
std::shared_ptr<QueryBuilder> query();
/*! Get a new raw query expression. */
inline Query::Expression raw(const QVariant &value) const;
/*! Get a new raw query expression. */
inline Query::Expression raw(QVariant &&value) const noexcept;
/* Running SQL Queries */
/*! Run a select statement against the database. */
SqlQuery
select(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run a select statement against the database. */
inline SqlQuery
selectFromWriteConnection(const QString &queryString,
QVector<QVariant> bindings = {});
/*! Run a select statement and return a single result. */
SqlQuery
selectOne(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run a select statement and return the first column of the first row. */
QVariant
scalar(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run an insert statement against the database. */
inline SqlQuery
insert(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run an update statement against the database. */
inline std::tuple<int, QSqlQuery>
update(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run a delete statement against the database. */
inline std::tuple<int, QSqlQuery>
remove(const QString &queryString, QVector<QVariant> bindings = {});
/*! Execute an SQL statement, should be used for DDL/DML queries, internally
calls DatabaseConnection::recordsHaveBeenModified(). */
SqlQuery statement(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run an SQL statement and get the number of rows affected (for DML queries). */
std::tuple<int, QSqlQuery>
affectingStatement(const QString &queryString, QVector<QVariant> bindings = {});
/*! Run a raw, unprepared query against the database (good for DDL queries). */
SqlQuery unprepared(const QString &queryString);
/* Obtain connection instance */
/*! Get underlying database connection (QSqlDatabase). */
QSqlDatabase getQtConnection();
/*! Get underlying database connection without executing any reconnect logic. */
QSqlDatabase getRawQtConnection() const;
/*! Get the connection resolver for an underlying database connection. */
inline const std::function<Connectors::ConnectionName()> &
getQtConnectionResolver() const noexcept;
/*! Set the connection resolver for an underlying database connection. */
DatabaseConnection &setQtConnectionResolver(
const std::function<Connectors::ConnectionName()> &resolver);
/*! Get a new QSqlQuery instance for the current connection. */
QSqlQuery getQtQuery();
/*! Prepare the query bindings for execution. */
QVector<QVariant> &prepareBindings(QVector<QVariant> &bindings) const;
/*! Bind values to their parameters in the given statement. */
static void bindValues(QSqlQuery &query, const QVector<QVariant> &bindings);
/*! Determine whether the database connection is currently open. */
inline bool isOpen();
/*! Check database connection and show warnings when the state changed. */
virtual bool pingDatabase();
/*! Returns the database driver used to access the database connection. */
QSqlDriver *driver();
/*! Force connection to the database (creates physical connection), doesn't have
to be called before querying a database. */
inline void connectEagerly();
/*! Reconnect to the database if a Qt connection is missing (doesn't create
a physical connection, only refreshs connection resolver). */
void reconnectIfMissingConnection() const;
/*! Reconnect to the database (doesn't create physical connection, only refreshes
a connection resolver). */
void reconnect() const;
/*! Disconnect from the underlying Qt's connection. */
void disconnect();
/*! Get the query grammar used by the connection. */
inline const QueryGrammar &getQueryGrammar() const noexcept;
/*! Get the query grammar used by the connection. */
inline QueryGrammar &getQueryGrammar() noexcept;
/*! Get the schema grammar used by the connection. */
inline const SchemaGrammar &getSchemaGrammar();
/*! Get the schema builder used by the connection. */
SchemaBuilder &getSchemaBuilder();
/*! Get the query post processor used by the connection. */
inline const QueryProcessor &getPostProcessor() const noexcept;
/*! Get the query grammar used by the connection as a std::shared_ptr. */
inline std::shared_ptr<QueryGrammar> getQueryGrammarShared() const noexcept;
/*! Get the schema grammar used by the connection as a std::shared_ptr. */
std::shared_ptr<SchemaGrammar> getSchemaGrammarShared();
/*! Set the reconnect instance on the connection. */
DatabaseConnection &setReconnector(const ReconnectorType &reconnector);
/* Connection configuration */
/*! Get an option value from the configuration options. */
QVariant getConfig(const QString &option) const;
/*! Get the configuration for the current connection. */
inline const QVariantHash &getConfig() const noexcept;
/*! Check whether the configuration contains the given option. */
bool hasConfig(const QString &option) const;
/* Getters */
/*! Return the connection's driver name. */
QString driverName();
/*! Return connection's driver name in printable format eg. QMYSQL -> MySQL. */
const QString &driverNamePrintable();
/*! Get the database connection name. */
inline const QString &getName() const noexcept;
/*! Get the name of the connected database. */
inline const QString &getDatabaseName() const noexcept;
/*! Get the hostname of the connected database. */
inline const QString &getHostName() const noexcept;
/*! Get the QtTimeZoneConfig for the current connection. */
inline const QtTimeZoneConfig &getQtTimeZone() const noexcept;
/*! Set the QtTimeZoneConfig for the current connection (override qt_timezone). */
DatabaseConnection &setQtTimeZone(const QVariant &timezone);
/*! Set the QtTimeZoneConfig for the current connection (override qt_timezone). */
DatabaseConnection &setQtTimeZone(QtTimeZoneConfig &&timezone) noexcept;
/*! Determine whether the QDateTime time zone should be converted. */
inline bool isConvertingTimeZone() const noexcept;
/* Others */
/*! Execute the given callback in "dry run" mode. */
QVector<Log> pretend(const std::function<void()> &callback);
/*! Execute the given callback in "dry run" mode. */
QVector<Log> pretend(const std::function<void(DatabaseConnection &)> &callback);
/*! Determine if the connection is in a "dry run". */
inline bool pretending() const;
/*! Check if any records have been modified. */
inline bool getRecordsHaveBeenModified() const;
/*! Indicates if any records have been modified. */
inline void recordsHaveBeenModified(bool value = true);
/*! Reset the record modification state. */
inline void forgetRecordModificationState();
protected:
/*! Set the query grammar to the default implementation. */
void useDefaultQueryGrammar();
/*! Set the schema grammar to the default implementation. */
void useDefaultSchemaGrammar();
/*! Set the schema builder to the default implementation. */
void useDefaultSchemaBuilder();
/*! Set the query post processor to the default implementation. */
void useDefaultPostProcessor();
/*! Get the default query grammar instance. */
virtual std::unique_ptr<QueryGrammar> getDefaultQueryGrammar() const = 0;
/*! Get the default schema grammar instance. */
virtual std::unique_ptr<SchemaGrammar> getDefaultSchemaGrammar() = 0;
/*! Get the default schema builder instance. */
virtual std::unique_ptr<SchemaBuilder> getDefaultSchemaBuilder() = 0;
/*! Get the default post processor instance. */
virtual std::unique_ptr<QueryProcessor> getDefaultPostProcessor() const = 0;
/*! Callback type used in the run() method. */
template<typename Return>
using RunCallback = std::function<Return(const QString &,
const QVector<QVariant> &)>;
/*! Run a SQL statement and log its execution context. */
template<typename Return>
Return run(
const QString &queryString, QVector<QVariant> &&bindings,
const QString &type, const RunCallback<Return> &callback);
/*! Run a SQL statement. */
template<typename Return>
Return runQueryCallback(
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const;
/*! The active QSqlDatabase connection name. */
std::optional<Connectors::ConnectionName> m_qtConnection = std::nullopt;
/*! The QSqlDatabase connection resolver. */
std::function<Connectors::ConnectionName()> m_qtConnectionResolver;
/*! The name of the connected database. */
/*const*/ QString m_database;
/*! The table prefix for the connection. */
QString m_tablePrefix;
/*! Determine how the QDateTime time zone will be converted. */
QtTimeZoneConfig m_qtTimeZone;
/*! Determine whether the QDateTime time zone should be converted. */
bool m_isConvertingTimeZone;
/*! Determine whether to return the QDateTime/QDate or QString (SQLite only). */
std::optional<bool> m_returnQDateTime = std::nullopt;
/*! The database connection configuration options. */
/*const*/ QVariantHash m_config;
/*! The reconnector instance for the connection. */
ReconnectorType m_reconnector = nullptr;
/*! The query grammar implementation. */
std::shared_ptr<QueryGrammar> m_queryGrammar = nullptr;
/*! The schema grammar implementation. */
std::shared_ptr<SchemaGrammar> m_schemaGrammar = nullptr;
/*! The schema builder implementation. */
std::unique_ptr<SchemaBuilder> m_schemaBuilder = nullptr;
/*! The query post processor implementation. */
std::unique_ptr<QueryProcessor> m_postProcessor = nullptr;
/* Others */
/*! Indicates if the connection is in a "dry run". */
bool m_pretending = false;
private:
/*! Prepare an SQL statement and return the query object. */
QSqlQuery prepareQuery(const QString &queryString);
/*! Get a new QSqlQuery instance for the pretend for the current connection. */
inline QSqlQuery getQtQueryForPretend();
/*! Prepare the QDateTime query binding for execution. */
QDateTime prepareBinding(const QDateTime &binding) const;
/*! Handle a query exception. */
template<typename Return>
Return handleQueryException(
const std::exception_ptr &ePtr, const Exceptions::QueryError &e,
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const;
/*! Handle a query exception that occurred during query execution. */
template<typename Return>
Return tryAgainIfCausedByLostConnection(
const std::exception_ptr &ePtr, const Exceptions::QueryError &e,
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const;
/*! Determine if the elapsed time for queries should be counted. */
inline bool shouldCountElapsed() const;
/*! Log database connected, invoked during MySQL ping. */
void logConnected();
/*! Log database disconnected, invoked during MySQL ping. */
void logDisconnected();
/*! The flag for the database was disconnected, used during MySQL ping. */
bool m_disconnectedLogged = false;
/*! The flag for the database was connected, used during MySQL ping. */
bool m_connectedLogged = false;
/*! Connection name, obtained from the connection configuration. */
QString m_connectionName;
/*! Host name, obtained from the connection configuration. */
QString m_hostName;
/*! Connection's driver name in printable format eg. QMYSQL -> MySQL. */
std::optional<std::reference_wrapper<const QString>>
m_driverNamePrintable = std::nullopt;
};
#if defined(__GNUG__) && !defined(__clang__)
# pragma GCC diagnostic pop
#endif
/* public */
DatabaseConnection::~DatabaseConnection() = default;
QString DatabaseConnection::getTablePrefix() const
{
return m_tablePrefix;
}
Query::Expression
DatabaseConnection::raw(const QVariant &value) const // NOLINT(readability-convert-member-functions-to-static)
{
return Query::Expression(value);
}
Query::Expression
DatabaseConnection::raw(QVariant &&value) const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return Query::Expression(std::move(value));
}
/* Running SQL Queries */
SqlQuery
DatabaseConnection::selectFromWriteConnection(const QString &queryString,
QVector<QVariant> bindings)
{
// This member function is used from the schema builders/post-processors only
// FEATURE read/write connection silverqx
return select(queryString, std::move(bindings)/*, false*/);
}
SqlQuery
DatabaseConnection::insert(const QString &queryString, QVector<QVariant> bindings)
{
return statement(queryString, std::move(bindings));
}
std::tuple<int, QSqlQuery>
DatabaseConnection::update(const QString &queryString, QVector<QVariant> bindings)
{
return affectingStatement(queryString, std::move(bindings));
}
std::tuple<int, QSqlQuery>
DatabaseConnection::remove(const QString &queryString, QVector<QVariant> bindings)
{
return affectingStatement(queryString, std::move(bindings));
}
/* Obtain connection instance */
const std::function<Connectors::ConnectionName()> &
DatabaseConnection::getQtConnectionResolver() const noexcept
{
return m_qtConnectionResolver;
}
bool DatabaseConnection::isOpen()
{
return m_qtConnection && getQtConnection().isOpen();
}
void DatabaseConnection::connectEagerly()
{
reconnectIfMissingConnection();
// This opens a physical database connection
std::ignore = getQtConnection();
}
const QueryGrammar &DatabaseConnection::getQueryGrammar() const noexcept
{
return *m_queryGrammar;
}
QueryGrammar &DatabaseConnection::getQueryGrammar() noexcept
{
return *m_queryGrammar;
}
const SchemaGrammar &DatabaseConnection::getSchemaGrammar()
{
return *getSchemaGrammarShared();
}
const QueryProcessor &DatabaseConnection::getPostProcessor() const noexcept
{
return *m_postProcessor;
}
std::shared_ptr<QueryGrammar>
DatabaseConnection::getQueryGrammarShared() const noexcept
{
return m_queryGrammar;
}
const QVariantHash &DatabaseConnection::getConfig() const noexcept
{
return m_config;
}
/* Getters */
const QString &DatabaseConnection::getName() const noexcept
{
return m_connectionName;
}
const QString &DatabaseConnection::getDatabaseName() const noexcept
{
return m_database;
}
const QString &DatabaseConnection::getHostName() const noexcept
{
return m_hostName;
}
const QtTimeZoneConfig &DatabaseConnection::getQtTimeZone() const noexcept
{
return m_qtTimeZone;
}
bool DatabaseConnection::isConvertingTimeZone() const noexcept
{
return m_isConvertingTimeZone;
}
/* Others */
bool DatabaseConnection::pretending() const
{
return m_pretending;
}
bool DatabaseConnection::getRecordsHaveBeenModified() const
{
return m_recordsModified;
}
void DatabaseConnection::recordsHaveBeenModified(const bool value)
{
m_recordsModified = value;
}
void DatabaseConnection::forgetRecordModificationState()
{
m_recordsModified = false;
}
/* protected */
template<typename Return>
Return
DatabaseConnection::run(
const QString &queryString, QVector<QVariant> &&bindings, // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
const QString &type, const RunCallback<Return> &callback)
{
reconnectIfMissingConnection();
// Is Elapsed timer needed?
const auto countElapsed = shouldCountElapsed();
QElapsedTimer timer;
if (countElapsed)
timer.start();
/* Prepare bindings early so they will be prepared only once (for performance
reasons). The weird preparedBindings() return value is for better variable
naming. */
const auto &preparedBindings = prepareBindings(bindings);
/* Here we will run this query. If an exception occurs we'll determine if it was
caused by a connection that has been lost. If that is the cause, we'll try
to re-establish connection and re-run the query with a fresh connection. */
Return result = std::invoke([this, &queryString, &preparedBindings, &callback]
{
try {
return runQueryCallback(queryString, preparedBindings, callback);
} catch (const Exceptions::QueryError &e) {
return handleQueryException(std::current_exception(), e,
queryString, preparedBindings, callback);
}
});
std::optional<qint64> elapsed;
if (countElapsed) {
// Hit elapsed timer
elapsed = timer.elapsed();
// Queries execution time counter
m_elapsedCounter += *elapsed;
}
/* Once we have run the query we will calculate the time that it took
to run and then log the query, bindings, and execution time. We'll
log time in milliseconds. */
if (m_pretending)
logQueryForPretend(queryString, preparedBindings, type);
else
logQuery(result, elapsed, type);
return result;
}
template<typename Return>
Return
DatabaseConnection::runQueryCallback(
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const
{
/* To execute the statement, we'll simply call the callback, which will actually
run the SQL against the QSqlDatabase connection. Then we can calculate the time
it took to execute and log the query SQL, bindings and time in our memory. */
return std::invoke(callback, queryString, preparedBindings);
}
/* private */
QSqlQuery DatabaseConnection::getQtQueryForPretend()
{
return getQtQuery();
}
template<typename Return>
Return
DatabaseConnection::handleQueryException(
const std::exception_ptr &ePtr, const Exceptions::QueryError &e,
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const
{
// FUTURE add info about in transaction into the exception that it was a reason why connection was not reconnected/recovered silverqx
if (inTransaction())
std::rethrow_exception(ePtr);
return tryAgainIfCausedByLostConnection(ePtr, e, queryString, preparedBindings,
callback);
}
template<typename Return>
Return
DatabaseConnection::tryAgainIfCausedByLostConnection(
const std::exception_ptr &ePtr, const Exceptions::QueryError &e,
const QString &queryString, const QVector<QVariant> &preparedBindings,
const RunCallback<Return> &callback) const
{
// TODO would be good to call KILL on lost connection to free locks, https://dev.mysql.com/doc/c-api/8.0/en/c-api-auto-reconnect.html silverqx
if (causedByLostConnection(e)) {
reconnect();
// BUG rethrow e when causedByLostConnection to correctly inform user, causedByLostConnection state lost during second runQueryCallback(), because it internally tries to connect to DB and throws "Unable to connect to database" instead of "Lost connection", probably another try-catch and if catched "Unable to connect to database" then rethrow e (Lost connection)? silverqx
/* After the second failed attempt will be isOpen() == false because
the m_qtConnection == std::nullopt. */
return runQueryCallback(queryString, preparedBindings, callback);
}
std::rethrow_exception(ePtr);
}
bool DatabaseConnection::shouldCountElapsed() const
{
return !m_pretending && (m_debugSql || m_countingElapsed);
}
} // namespace Orm
TINYORM_END_COMMON_NAMESPACE
#endif // ORM_DATABASECONNECTION_HPP