Merge pull request #41 from QuasarApp/dbPatches

Added support Database patches
This commit is contained in:
Andrei Yankovich 2021-10-11 10:54:18 +03:00 committed by GitHub
commit 38366cfa32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 368 additions and 23 deletions

View File

@ -34,6 +34,14 @@ public:
*/
using Job = std::function<bool()>;
/**
* @brief asyncLauncher This method invoke a job on the thread (using the asyncHandler method) of this object.
* @param job This is function with needed job.
* @param await This is boolean option for enable or disable wait for finish of the job function.
* @return true if the job function started correctly. If the await option is true then
* this method return result of job function.
*/
bool asyncLauncher(const Job &job, bool await = false) const;
protected:
/**
@ -66,14 +74,6 @@ protected:
*/
bool waitFor(const Job &condition, int timeout = WAIT_TIME) const;
/**
* @brief asyncLauncher This method invoke a job on the thread (using the asyncHandler method) of this object.
* @param job This is function with needed job.
* @param await This is boolean option for enable or disable wait for finish of the job function.
* @return true if the job function started correctly. If the await option is true then
* this method return result of job function.
*/
bool asyncLauncher(const Job &job, bool await = false) const;
private slots:
/**

View File

@ -29,5 +29,11 @@ CREATE TABLE IF NOT EXISTS DefaultPermissions (
FOREIGN KEY(dbAddress) REFERENCES MemberPermisions(dbAddress)
ON UPDATE CASCADE
ON DELETE CASCADE
)
);
CREATE TABLE IF NOT EXISTS DataBaseAttributes (
name TEXT NOT NULL PRIMARY KEY,
value INT NOT NULL UNIQUE
);

View File

@ -32,3 +32,8 @@ CREATE TABLE IF NOT EXISTS DefaultPermissions (
ON UPDATE CASCADE
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS DataBaseAttributes (
name TEXT NOT NULL PRIMARY KEY,
value INT NOT NULL UNIQUE
);

View File

@ -34,6 +34,8 @@
#include <sqlitedbcache.h>
#include <sqldb.h>
#include <QCryptographicHash>
#include "getsinglevalue.h"
#include "setsinglevalue.h"
#define THIS_NODE "this_node_key"
namespace QH {
@ -64,10 +66,22 @@ bool DataBaseNode::initSqlDb(QString DBparamsFile,
if (DBparamsFile.isEmpty()) {
params = defaultDbParams();
return _db->init(params);
if (!_db->init(params)) {
return false;
}
} else {
if (!_db->init(DBparamsFile)) {
return false;
}
}
if (!_db->init(DBparamsFile)) {
if (!upgradeDataBase()) {
QuasarAppUtils::Params::log("Failed to upgrade database",
QuasarAppUtils::Error);
return false;
}
@ -180,6 +194,10 @@ void DataBaseNode::objectChanged(const QSharedPointer<DBObject> &) {
}
DBPatchMap DataBaseNode::dbPatches() const {
return {};
}
void DataBaseNode::handleObjectChanged(const QSharedPointer<DBObject> &item) {
notifyObjectChanged(item.staticCast<PKG::ISubscribableData>());
objectChanged(item);
@ -371,6 +389,71 @@ bool DataBaseNode::isForbidenTable(const QString &table) {
return systemTables().contains(table);
}
bool DataBaseNode::upgradeDataBase() {
auto patches = dbPatches();
int actyalyVersion = patches.size();
int currentVersion = 0;
if (!db())
return false;
bool fsupportUpgrade = db()->doQuery("SELECT COUNT(*) FROM DataBaseAttributes", true);
if (!fsupportUpgrade) {
QuasarAppUtils::Params::log("The data base of application do not support soft upgrade. "
"Please remove database monyaly and restart application.",
QuasarAppUtils::Error);
return false;
}
PKG::GetSingleValue request({"DataBaseAttributes", "version"}, "value", "name");
if (auto responce = _db->getObject(request)) {
currentVersion = responce->value().toInt();
}
bool fUpdated = false;
while (currentVersion < actyalyVersion) {
auto patch = patches.value(currentVersion, {});
QString message;
if (currentVersion == 0) {
message = "Initialize data base!";
} else {
message = "Upgrade data base!. from %0 to %1 versions";
}
QuasarAppUtils::Params::log(message,
QuasarAppUtils::Info);
if (patch && !patch(db())) {
message = message.arg(currentVersion).arg(currentVersion);
QuasarAppUtils::Params::log("Failed to " + message,
QuasarAppUtils::Error);
return false;
}
currentVersion++;
fUpdated = true;
}
if (fUpdated) {
auto updateVersionRequest = QSharedPointer<PKG::SetSingleValue>::create(
DbAddress{"DataBaseAttributes", "version"},
"value", currentVersion, "name");
if (!_db->insertIfExistsUpdateObject(updateVersionRequest, true)) {
QuasarAppUtils::Params::log("Failed to update version attribute",
QuasarAppUtils::Error);
return false;
}
}
return true;
}
void DataBaseNode::setLocalNodeName(const QString &newLocalNodeName) {
_localNodeName = newLocalNodeName;
}

View File

@ -27,8 +27,22 @@ class SqlDBWriter;
class WebSocketController;
class DbAddress;
class NodeId;
class iObjectProvider;
/**
* @brief DBPatch This is function that should be upgrade database.
* @see DBPatchMap
* @see DataBaseNode::dbPatch
*/
typedef std::function<bool (const QH::iObjectProvider *)> DBPatch;
/**
* @brief DBPatchMap This is list when index of list is version of database and value if function that should be upgrade database.
* @see DataBaseNode::dbPatch
* @see DBPatchMap
*/
typedef QList<DBPatch> DBPatchMap;
/**
* @brief The BaseNode class is database base implementation of nodes or servers.
* This implementation contains methods for work with database.
@ -395,6 +409,40 @@ protected:
*/
virtual void objectChanged(const QSharedPointer<PKG::DBObject>& obj);
/**
* @brief dbPatches This method should be return map with functions that upgrade production data base.
* Eeach function shoul be can upgrade databae from preview version to own version.
* **Example**:
*
* We have 4 version of data base {0, 1, 2, 3} each version should be contains own function for upgrade data base.
* Where the 0 version is first version of database. (genesis)
*
* @code{cpp}
* QH::DBPatchMap dbPatches() const {
QH::DBPatchMap result;
result += [](const QH::iObjectProvider* database) -> bool {
// Some code for update from 0 to 1
};
result += [](const QH::iObjectProvider* database) -> bool {
// Some code for update from 1 to 2
};
result += [](const QH::iObjectProvider* database) -> bool {
// Some code for update from 2 to 3
};
return result;
}
* @endcode
*
* @return Map of database pactches.
*
* @see DBPatchMap
* @see DBPatch
*/
virtual DBPatchMap dbPatches() const;
private slots:
void handleObjectChanged(const QSharedPointer<PKG::DBObject> &item);
@ -415,6 +463,13 @@ private:
bool isForbidenTable(const QString& table);
/**
* @brief upgradeDataBase This method upgrade data base to actyaly database version.
* @note The last version of dbPatches is actyaly version.
* @return true if operation finished successful
*/
bool upgradeDataBase();
ISqlDBCache *_db = nullptr;
QString _localNodeName;
WebSocketController *_webSocketWorker = nullptr;

View File

@ -120,6 +120,14 @@ public:
bool insertIfExistsUpdateObject(const QSharedPointer<QH::PKG::DBObject>& saveObject,
bool wait = true);
/**
* @brief doQuery This method execute a @a query in this database.
* @param query This is query that will be executed.
* @param result This is query result value.
* @warning The result works onlt on await mode. Set the @a wait param to true.
* @return true if the query finished successful
*/
virtual bool doQuery(const QString& query, bool wait = false, QSqlQuery* result = nullptr) const = 0;
};

View File

@ -214,6 +214,16 @@ bool ISqlDBCache::insertObject(const QSharedPointer<DBObject> &saveObject, bool
return true;
}
bool ISqlDBCache::doQuery(const QString &query, bool wait,
QSqlQuery *result) const {
if (!_writer) {
return false;
}
return _writer->doQuery(query, wait, result);
}
bool ISqlDBCache::init(const QString &initDbParams) {
if (!_writer) {

View File

@ -107,6 +107,8 @@ public:
bool insertObject(const QSharedPointer<QH::PKG::DBObject>& saveObject,
bool wait = false) override;
bool doQuery(const QString &query, bool wait = false, QSqlQuery* result = nullptr) const override;
/**
* @brief changeObjects This method change object of the database.
* @param templateObject This is template for get objects from database.
@ -258,6 +260,7 @@ signals:
*/
void sigItemDeleted(const DbAddress& obj);
};
/**

View File

@ -14,10 +14,11 @@ namespace QH {
namespace PKG {
GetSingleValue::GetSingleValue(const DbAddress& address, const QString& field):
GetSingleValue::GetSingleValue(const DbAddress& address, const QString& field, const QString& primaryKey):
DBObject(address) {
_field = field;
_key = primaryKey;
}
QVariant GetSingleValue::value() const {
@ -29,9 +30,9 @@ DBObject *GetSingleValue::createDBObject() const {
}
PrepareResult GetSingleValue::prepareSelectQuery(QSqlQuery &q) const {
QString queryString = "SELECT %0 FROM %1 WHERE id='%2'";
QString queryString = "SELECT %0 FROM %1 WHERE %2='%3'";
queryString = queryString.arg(_field, tableName(), getId().toString());
queryString = queryString.arg(_field, tableName(), _key, getId().toString());
if (!q.prepare(queryString)) {
return PrepareResult::Fail;
@ -51,7 +52,7 @@ bool GetSingleValue::isCached() const {
}
QString GetSingleValue::primaryKey() const {
return "";
return _key;
}
}

View File

@ -18,7 +18,7 @@ namespace PKG {
/**
* @brief The GetSingleValue class is intended for get a single value from database.
* The value is selected by id.
* The value is selected by primaryKey. By Default the primary key is 'id'
*/
class HEARTSHARED_EXPORT GetSingleValue final: public DBObject
{
@ -29,8 +29,9 @@ public:
* @brief GetSingleValue This is default constructor of the GetMaxIntegerId class.
* @param address This is address of getting object.
* @param field This is name of field.
* @param primaryKey This is primary key that will be using in selected query.
*/
GetSingleValue(const DbAddress& address, const QString& field);
GetSingleValue(const DbAddress& address, const QString& field, const QString& primaryKey = "id");
/**
* @brief value This method return Maximum value of a sql tables field.
@ -50,6 +51,7 @@ protected:
private:
QString _field;
QVariant _value;
QString _key;
};
}

View File

@ -6,7 +6,8 @@
*/
#include "setsinglevalue.h"
#include "quasarapp.h"
#include <QSqlError>
#include <QSqlQuery>
namespace QH {
@ -17,11 +18,13 @@ namespace PKG {
SetSingleValue::SetSingleValue(const DbAddress& address,
const QString& field,
const QVariant& value):
const QVariant& value,
const QString &primaryKey):
DBObject(address)
{
_field = field;
_value = value;
_primaryKey = primaryKey;
}
DBObject *SetSingleValue::createDBObject() const {
@ -29,12 +32,14 @@ DBObject *SetSingleValue::createDBObject() const {
}
PrepareResult SetSingleValue::prepareUpdateQuery(QSqlQuery &q) const {
QString queryString = "UPDATE %0 SET %1=:%1 WHERE id='%2'";
QString queryString = "UPDATE %0 SET %1=:%1 WHERE %2='%3'";
queryString = queryString.arg(tableName(), _field, getId().toString());
queryString = queryString.arg(tableName(), _field, primaryKey(), getId().toString());
if (!q.prepare(queryString)) {
QuasarAppUtils::Params::log("Failed to prepare query: " + q.lastError().text(),
QuasarAppUtils::Error);
return PrepareResult::Fail;
}
@ -43,6 +48,24 @@ PrepareResult SetSingleValue::prepareUpdateQuery(QSqlQuery &q) const {
return PrepareResult::Success;
}
PrepareResult SetSingleValue::prepareInsertQuery(QSqlQuery &q) const {
QString queryString = "INSERT INTO %0 (%1, %2) VALUES (:%1, :%2)";
queryString = queryString.arg(tableName(), primaryKey(), _field, getId().toString());
if (!q.prepare(queryString)) {
QuasarAppUtils::Params::log("Failed to prepare query: " + q.lastError().text(),
QuasarAppUtils::Error);
return PrepareResult::Fail;
}
q.bindValue(":" + primaryKey(), getId().toString());
q.bindValue(":" + _field, _value);
return PrepareResult::Success;
}
bool SetSingleValue::fromSqlRecord(const QSqlRecord &) {
return true;
}
@ -52,7 +75,7 @@ bool SetSingleValue::isCached() const {
}
QString SetSingleValue::primaryKey() const {
return "";
return _primaryKey;
}
}
}

View File

@ -38,13 +38,17 @@ public:
* @param address This is address of the field intended for update object.
* @param field This is field id (column name ) of the intended for update object.
* @param value This is a new value.
* @param primaryKey This is primary key that will be used for insert of update value. By Default is is id
*/
SetSingleValue(const DbAddress& address,
const QString& field,
const QVariant& value);
const QVariant& value,
const QString& primaryKey = "id");
DBObject *createDBObject() const override;
PrepareResult prepareUpdateQuery(QSqlQuery &q) const override;
PrepareResult prepareInsertQuery(QSqlQuery &q) const override;
bool fromSqlRecord(const QSqlRecord &q) override;
bool isCached() const override;
@ -54,6 +58,7 @@ protected:
private:
QString _field;
QString _primaryKey;
QVariant _value;
};

View File

@ -131,6 +131,26 @@ bool SqlDBWriter::initDbPrivate(const QVariantMap &params) {
return initSuccessful;
}
bool SqlDBWriter::doQueryPrivate(const QString &query, QSqlQuery* result) const {
if (!db()) {
return false;
}
QSqlQuery q(query, *db());
if (!q.exec()) {
QuasarAppUtils::Params::log("request error : " + q.lastError().text());
return false;
}
if (result) {
*result = q;
}
return true;
}
bool SqlDBWriter::enableFK() {
if (!db()) {
return false;
@ -317,6 +337,18 @@ bool SqlDBWriter::insertQuery(const QSharedPointer<DBObject> &ptr) const {
return workWithQuery(q, prepare, cb, ptr->printError());
}
bool SqlDBWriter::doQuery(const QString &query,
bool wait, QSqlQuery* result) const {
wait = result || wait;
Async::Job job = [this, query, result]() {
return doQueryPrivate(query, result);
};
return asyncLauncher(job, wait);
}
bool SqlDBWriter::selectQuery(const DBObject& requestObject,
QList<QSharedPointer<QH::PKG::DBObject>> &result) {

View File

@ -69,6 +69,7 @@ public:
bool deleteObject(const QSharedPointer<PKG::DBObject> &ptr, bool wait = false) override;
bool insertObject(const QSharedPointer<PKG::DBObject> &ptr, bool wait = false) override;
void setSQLSources(const QStringList &list) override;
bool doQuery(const QString& query, bool wait = false, QSqlQuery *result = nullptr) const override;
/**
* @brief databaseLocation This method return location of database.
@ -213,6 +214,14 @@ private:
*/
bool initDbPrivate(const QVariantMap &params);
/**
* @brief doQueryPrivate This method execute a @a query in this database.
* @param query This is query that will be executed.
* @param result This is pointer to result value.
* @return true if query finished successfull
*/
bool doQueryPrivate(const QString& query, QSqlQuery *result) const;
bool initSuccessful = false;
QVariantMap _config;
QStringList _SQLSources;

View File

@ -36,8 +36,10 @@ protected:
_ping.setAnsver(ping->ansver());
}
private:
QH::PKG::Ping _ping;
};
class BadTstClient: public QH::DataBaseNode {

View File

@ -0,0 +1,83 @@
#include "upgradedatabasetest.h"
#include "databasenode.h"
#include <QSqlQuery>
#include <QSqlResult>
#include <iobjectprovider.h>
#include <QtTest>
#include <isqldbcache.h>
#define LOCAL_TEST_PORT TEST_PORT + 5
class UpgradableDatabase: public QH::DataBaseNode {
// DataBaseNode interface
public:
bool checkVersion(int version) {
QSqlQuery query;
if (!db()->doQuery("SELECT * FROM DataBaseAttributes WHERE name='version'", true, &query)){
return false;
};
if (!query.next()) {
return false;
}
int ver = query.value("value").toInt();
return ver == version;
}
protected:
QH::DBPatchMap dbPatches() const {
QH::DBPatchMap result;
result += [](const QH::iObjectProvider* database) -> bool {
QSqlQuery query;
if (!database->doQuery("select * from DataBaseAttributes", true, &query)){
return false;
};
return true;
};
result += [](const QH::iObjectProvider* database) -> bool {
QSqlQuery query;
if (!database->doQuery("select * from DataBaseAttributes", true, &query)){
return false;
};
return true;
};
result += [](const QH::iObjectProvider* database) -> bool {
QSqlQuery query;
if (!database->doQuery("select * from DataBaseAttributes", true, &query)){
return false;
};
return true;
};
return result;
}
};
UpgradeDataBaseTest::UpgradeDataBaseTest() {
}
void UpgradeDataBaseTest::test() {
UpgradableDatabase *db = new UpgradableDatabase();
QVERIFY(db->run(TEST_LOCAL_HOST, LOCAL_TEST_PORT, "UpgradeDBTest"));
QVERIFY(db->checkVersion(3));
db->stop();
QVERIFY(db->run(TEST_LOCAL_HOST, LOCAL_TEST_PORT, "UpgradeDBTest"));
db->softDelete();
}

View File

@ -0,0 +1,16 @@
#ifndef UPGRADEDATABASETEST_H
#define UPGRADEDATABASETEST_H
#include "test.h"
#include "basetestutils.h"
class UpgradeDataBaseTest: public Test, protected BaseTestUtils
{
public:
UpgradeDataBaseTest();
// Test interface
public:
void test();
};
#endif // UPGRADEDATABASETEST_H

View File

@ -15,6 +15,7 @@
#include <basenodetest.h>
#include <bigdatatest.h>
#include <singleservertest.h>
#include <upgradedatabasetest.h>
#endif
#if HEART_BUILD_LVL >= 2
#include <networknodetest.h>
@ -43,6 +44,7 @@ private slots:
#if HEART_BUILD_LVL >= 1
TestCase(baseNodeTest, BaseNodeTest)
TestCase(singleNodeTest, SingleServerTest)
TestCase(upgradeDataBaseTest, UpgradeDataBaseTest)
#endif
#if HEART_BUILD_LVL >= 2
TestCase(networkNodeTest, NetworkNodeTest)