Commit 5251bf3a4b053b7e27754dce13ada5611ba6582b

Authored by Steven de Ridder
0 parents

Initial commit. dependencies not resolved yet.

.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +build/
  2 +CMakeLists.txt.user
CMakeLists.txt 0 → 100644
  1 +++ a/CMakeLists.txt
  1 +cmake_minimum_required(VERSION 3.0)
  2 +
  3 +# Check to see where cmake is located.
  4 +if( IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/cmake )
  5 + LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
  6 +elseif( IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../cmake )
  7 + LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake)
  8 +else()
  9 + return()
  10 +endif()
  11 +
  12 +# Check to see if there is versioning information available
  13 +if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/osdev_versioning/cmake)
  14 + LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/osdev_versioning/cmake)
  15 + include(osdevversion)
  16 +endif()
  17 +
  18 +include(projectheader)
  19 +project_header(osdev_orm)
  20 +
  21 +add_subdirectory(src)
  22 +add_subdirectory(tests)
  23 +
  24 +# include(packaging)
  25 +# package_component()
README.md 0 → 100644
  1 +++ a/README.md
src/CMakeLists.txt 0 → 100644
  1 +++ a/src/CMakeLists.txt
  1 +cmake_minimum_required(VERSION 3.0)
  2 +include(projectheader)
  3 +project_header(orm)
  4 +
  5 +find_package( Qt5Core REQUIRED )
  6 +find_package( Qt5Sql REQUIRED )
  7 +
  8 +include_directories( SYSTEM
  9 + ${Qt5Core_INCLUDE_DIRS}
  10 + ${Qt5Sql_INCLUDE_DIRS}
  11 +)
  12 +
  13 +include(compiler)
  14 +
  15 +include_directories(
  16 + ${CMAKE_CURRENT_SOURCE_DIR}/../config
  17 + ${CMAKE_CURRENT_SOURCE_DIR}/../global
  18 + ${CMAKE_CURRENT_SOURCE_DIR}/../logutils
  19 + ${CMAKE_CURRENT_SOURCE_DIR}/../../interfaces
  20 + ${CMAKE_CURRENT_SOURCE_DIR}/../datatypes
  21 + ${CMAKE_CURRENT_SOURCE_DIR}/../dbconnector
  22 + ${CMAKE_CURRENT_SOURCE_DIR}/../transqueue
  23 + ${CMAKE_CURRENT_SOURCE_DIR}/../pugixml
  24 +)
  25 +
  26 +set(SRC_LIST
  27 + ${CMAKE_CURRENT_SOURCE_DIR}/multisortfilterproxymodel.cpp
  28 + ${CMAKE_CURRENT_SOURCE_DIR}/ormhandler.cpp
  29 + ${CMAKE_CURRENT_SOURCE_DIR}/ormthread.cpp
  30 + ${CMAKE_CURRENT_SOURCE_DIR}/ormtable.cpp
  31 + ${CMAKE_CURRENT_SOURCE_DIR}/ormtablerow.cpp
  32 + ${CMAKE_CURRENT_SOURCE_DIR}/ormbatchchange.cpp
  33 + ${CMAKE_CURRENT_SOURCE_DIR}/ormbatchchange.h
  34 + ${CMAKE_CURRENT_SOURCE_DIR}/multisortfilterproxymodel.h
  35 + ${CMAKE_CURRENT_SOURCE_DIR}/ormhandler.h
  36 + ${CMAKE_CURRENT_SOURCE_DIR}/ormthread.h
  37 + ${CMAKE_CURRENT_SOURCE_DIR}/ormtable.h
  38 + ${CMAKE_CURRENT_SOURCE_DIR}/ormtablerow.h
  39 + ${CMAKE_CURRENT_SOURCE_DIR}/timeline.cpp
  40 + ${CMAKE_CURRENT_SOURCE_DIR}/timeline.h
  41 + ${CMAKE_CURRENT_SOURCE_DIR}/timestamp.cpp
  42 + ${CMAKE_CURRENT_SOURCE_DIR}/timestamp.h
  43 +)
  44 +
  45 +include(qtmoc)
  46 +create_mocs( SRC_LIST MOC_LIST
  47 + ${CMAKE_CURRENT_SOURCE_DIR}/multisortfilterproxymodel.h
  48 + ${CMAKE_CURRENT_SOURCE_DIR}/ormhandler.h
  49 + ${CMAKE_CURRENT_SOURCE_DIR}/ormthread.h
  50 + ${CMAKE_CURRENT_SOURCE_DIR}/ormtable.h
  51 +)
  52 +
  53 +link_directories(
  54 + ${CMAKE_BINARY_DIR}/lib
  55 +)
  56 +
  57 +include(library)
  58 +add_libraries(
  59 + ${Qt5Core_LIBRARIES}
  60 + ${Qt5Sql_LIBRARIES}
  61 + global
  62 + logutils
  63 + datatypes
  64 + dbconnector
  65 + transqueue
  66 +)
  67 +
  68 +include(installation)
  69 +install_component()
src/multisortfilterproxymodel.cpp 0 → 100644
  1 +++ a/src/multisortfilterproxymodel.cpp
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +#include "multisortfilterproxymodel.h"
  23 +
  24 +#include "log.h"
  25 +
  26 +#include <QDebug>
  27 +
  28 +using namespace osdev::components;
  29 +
  30 +MultiSortFilterProxyModel::MultiSortFilterProxyModel(QObject* _parent)
  31 + : QSortFilterProxyModel(_parent)
  32 + , m_columnPatterns()
  33 +{
  34 +}
  35 +
  36 +void MultiSortFilterProxyModel::setFilterKeyColumns(const QList<qint32> &filterColumns)
  37 +{
  38 + m_columnPatterns.clear();
  39 + foreach(qint32 column, filterColumns)
  40 + {
  41 + m_columnPatterns.insert(column, QString()); // Add columns with empty patterns
  42 + }
  43 +}
  44 +
  45 +void MultiSortFilterProxyModel::setFilterFixedString(qint32 column, const QString &pattern)
  46 +{
  47 + if(!m_columnPatterns.contains(column))
  48 + {
  49 + return;
  50 + }
  51 +
  52 + m_columnPatterns[column] = pattern;
  53 +}
  54 +
  55 +bool MultiSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
  56 +{
  57 + if(m_columnPatterns.isEmpty())
  58 + {
  59 + return true; // No pattern -> accept all
  60 + }
  61 +
  62 + for(QMap<qint32, QString>::const_iterator iter = m_columnPatterns.constBegin(); iter != m_columnPatterns.constEnd(); ++iter)
  63 + {
  64 + QModelIndex idx = sourceModel()->index(sourceRow, iter.key(), sourceParent);
  65 + if (idx.data().toString() != iter.value())
  66 + {
  67 + return false;
  68 + }
  69 + }
  70 + return true;
  71 +}
src/multisortfilterproxymodel.h 0 → 100644
  1 +++ a/src/multisortfilterproxymodel.h
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +
  23 +#ifndef OSDEV_COMPONENTS_MULTISORTFILTERPROXYMODEL_H
  24 +#define OSDEV_COMPONENTS_MULTISORTFILTERPROXYMODEL_H
  25 +
  26 +#include <QObject>
  27 +#include <QMap>
  28 +#include <QSortFilterProxyModel>
  29 +#include <QString>
  30 +
  31 +namespace osdev {
  32 +namespace components {
  33 +
  34 +//! @brief Filter that can filter a model on multiple columns.
  35 +//! @note Do not mix uasge of setFilterKeyColumns and setFilterKeyColumn.
  36 +class MultiSortFilterProxyModel : public QSortFilterProxyModel
  37 +{
  38 + Q_OBJECT
  39 +
  40 +public:
  41 + explicit MultiSortFilterProxyModel(QObject* parent = 0);
  42 +
  43 + //! @brief Set the columns to filter on.
  44 + //! This will overwrite the previous filter settings (including the patterns).
  45 + void setFilterKeyColumns(const QList<qint32>& filterColumns);
  46 +
  47 + //! @brief Add a pattern on a specific column index.
  48 + //! If the index is not known the pattern is not applied.
  49 + void setFilterFixedString(qint32 column, const QString& pattern);
  50 +
  51 +protected:
  52 + //! @brief Called from the base class to do the actual filtering.
  53 + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const;
  54 +
  55 +private:
  56 + QMap<qint32, QString> m_columnPatterns; ///< holds the filter pattern for column indices.
  57 +};
  58 +
  59 +} // End namespace components
  60 +} // End namespace osdev
  61 +
  62 +#endif // OSDEV_COMPONENTS_MULTISORTFILTERPROXYMODEL_H
src/ormbatchchange.cpp 0 → 100644
  1 +++ a/src/ormbatchchange.cpp
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +#include "ormbatchchange.h"
  23 +#include "log.h"
  24 +#include <algorithm>
  25 +
  26 +using namespace osdev::components;
  27 +
  28 +OrmBatchChange::OrmBatchChange()
  29 + : m_changeTimestamp()
  30 + , m_foreignId()
  31 + , m_changeSet()
  32 + , m_valid(false)
  33 +{
  34 +}
  35 +
  36 +OrmBatchChange::OrmBatchChange(const Timestamp& ts, const QVariant& _foreignId, const std::set<QVariant>& _changeSet)
  37 + : m_changeTimestamp(ts)
  38 + , m_foreignId(_foreignId)
  39 + , m_changeSet(_changeSet)
  40 + , m_valid(!_foreignId.isNull() && ts.valid())
  41 +{
  42 + if (!m_valid)
  43 + {
  44 + LogInfo("[OrmBatchChange ctor]", QString("Change is invalid : Timestamp %1, foreignId::isNull %2").arg(m_changeTimestamp.toString()).arg(m_foreignId.isNull()));
  45 + }
  46 +}
  47 +
  48 +bool OrmBatchChange::processChange(const OrmBatchChange& change, bool exclusiveUpdate)
  49 +{
  50 + LogDebug("[OrmBatchChange::processChange]", QString("Check %1 against change with timestamp %2, exlusiveUpdate is %3")
  51 + .arg(m_changeTimestamp.toString())
  52 + .arg(change.timestamp().toString())
  53 + .arg(exclusiveUpdate));
  54 + if (!m_valid)
  55 + {
  56 + LogInfo("[OrmBatchChange::processChange]", "Incoming change is invalid");
  57 + return false; // This is an invalid change and can never become valid so signal to stop processing.
  58 + }
  59 +
  60 + if (*this < change)
  61 + {
  62 + if (exclusiveUpdate && (change.m_foreignId == m_foreignId))
  63 + {
  64 + // this change is not necessary because it is already superseded. Invalidate it and signal to stop processing.
  65 + LogInfo("[OrmBatchChange::processChange]", "Change is superseded by newer change");
  66 + m_valid = false;
  67 + return false;
  68 + }
  69 +
  70 + // Find the items that are in the incoming change but not in the already applied change that we are checking against.
  71 + // These items still need to change w.r.t to this change.
  72 + if (!m_changeSet.empty())
  73 + {
  74 + std::set<QVariant> result;
  75 + std::set_difference(m_changeSet.begin(), m_changeSet.end(), change.m_changeSet.begin(), change.m_changeSet.end(), std::inserter(result, result.begin()));
  76 + m_changeSet.swap(result);
  77 + }
  78 +
  79 + // If the changeset is empty and the update is not exclusive then nothing has to be done.
  80 + // In case of an exclusive update an empty set means that none of the items can point to the given foreignId
  81 + // and processing needs to be continued.
  82 + if (!exclusiveUpdate && m_changeSet.empty())
  83 + {
  84 + // Invalidate the incoming change and signal to stop processing.
  85 + LogInfo("[OrmBatchChange::processChange]", "Nothing to do for non exclusive update");
  86 + m_valid = false;
  87 + return false;
  88 + }
  89 + }
  90 + return true;
  91 +}
  92 +
  93 +// static
  94 +std::set<QVariant> OrmBatchChange::toSet(const QList<QVariant>& lst)
  95 +{
  96 + std::set<QVariant> result;
  97 + for (const auto& item : lst)
  98 + {
  99 + result.insert(item);
  100 + }
  101 + return result;
  102 +}
  103 +
  104 +// static
  105 +QList<QVariant> OrmBatchChange::toList(const std::set<QVariant>& s)
  106 +{
  107 + QList<QVariant> result;
  108 + for (const auto& item : s)
  109 + {
  110 + result.push_back(item);
  111 + }
  112 + return result;
  113 +}
  114 +
src/ormbatchchange.h 0 → 100644
  1 +++ a/src/ormbatchchange.h
  1 +#ifndef OSDEV_COMPONENTS_ORMBATCHCHANGE_H
  2 +#define OSDEV_COMPONENTS_ORMBATCHCHANGE_H
  3 +
  4 +#include <set>
  5 +
  6 +#include <QVariant>
  7 +
  8 +#include "timestamp.h"
  9 +
  10 +namespace osdev {
  11 +namespace components {
  12 +
  13 +/**
  14 + * @brief Describes a change that should happen or has happened on a BatchUpdate or MergeUpdate table.
  15 + */
  16 +class OrmBatchChange
  17 +{
  18 +public:
  19 + /**
  20 + * @brief Creates an invalid OrmBatchChange.
  21 + * @note The only way to make an OrmBatchChange valid is by assigning a valid OrmBatchChange to it.
  22 + */
  23 + OrmBatchChange();
  24 +
  25 + /**
  26 + * @brief Create an OrmBatchChange with a change description.
  27 + * @param ts A valid timestamp (not checked).
  28 + * @param foreignId The value that should be pointed to.
  29 + * @param changeSet The set of record identifiers of records that should point to the given foreignId.
  30 + */
  31 + OrmBatchChange(const Timestamp& ts, const QVariant& foreignId, const std::set<QVariant>& changeSet);
  32 +
  33 + // Default copyable and movable.
  34 + OrmBatchChange(const OrmBatchChange&) = default;
  35 + OrmBatchChange& operator=(const OrmBatchChange&) = default;
  36 + OrmBatchChange(OrmBatchChange&&) = default;
  37 + OrmBatchChange& operator=(OrmBatchChange&&) = default;
  38 +
  39 + /**
  40 + * @brief Process an already executed change on this change.
  41 + * The changeset of this object can be reshaped as a result.
  42 + * @param change A known change.
  43 + * @param exclusiveUpdate Flag that indicates if the change should be processed in an exclusive way.
  44 + * @return true to indicate this change is still valid and false otherwise.
  45 + * @note An exclusiveUpdate means that all records that are not mentioned in the change set will not point to the given foreignId.
  46 + */
  47 + bool processChange(const OrmBatchChange& change, bool exclusiveUpdate);
  48 +
  49 + /**
  50 + * @brief Changes are ordered w.r.t. each other by their timestamp.
  51 + */
  52 + bool operator<(const OrmBatchChange& rhs) const
  53 + {
  54 + return m_changeTimestamp < rhs.m_changeTimestamp;
  55 + }
  56 +
  57 + /**
  58 + * @brief Change the timestamp of this change.
  59 + * This is necessary when the change is recorded to be permanent. It then becomes the latest change.
  60 + * @param ts The new timestamp (no checks).
  61 + */
  62 + void setTimestamp(const Timestamp& ts)
  63 + {
  64 + m_changeTimestamp = ts;
  65 + }
  66 +
  67 + /**
  68 + * @return The timestamp of this change.
  69 + */
  70 + const Timestamp& timestamp() const
  71 + {
  72 + return m_changeTimestamp;
  73 + }
  74 +
  75 + /**
  76 + * @return The foreinId of this change.
  77 + */
  78 + QVariant foreignId() const
  79 + {
  80 + return m_foreignId;
  81 + }
  82 +
  83 + /**
  84 + * @return The change set of this change.
  85 + */
  86 + std::set<QVariant> changeSet() const
  87 + {
  88 + return m_changeSet;
  89 + }
  90 +
  91 + /**
  92 + * @return true if this change is valid, false otherwise.
  93 + */
  94 + bool valid() const
  95 + {
  96 + return m_valid;
  97 + }
  98 +
  99 + /**
  100 + * @brief Static method to convert a QList<QVariant> to an std::set<QVariant>.
  101 + * @param lst The QList<QVariant> to convert.
  102 + * @return the converted set.
  103 + */
  104 + static std::set<QVariant> toSet(const QList<QVariant>& lst);
  105 +
  106 + /**
  107 + * @brief Static method to convert an std::set<QVariant> to a QList<QVariant>.
  108 + * @param variantSet The set of variants to convert.
  109 + * @return the converted QList.
  110 + */
  111 + static QList<QVariant> toList(const std::set<QVariant>& variantSet);
  112 +
  113 +private:
  114 + Timestamp m_changeTimestamp; ///< The timestamp of this change.
  115 + QVariant m_foreignId; ///< The foreignId that should be pointed to.
  116 + std::set<QVariant> m_changeSet; ///< The set of record identifiers that describe the change.
  117 + bool m_valid; ///< Flag that indicates if this change is valid.
  118 +};
  119 +
  120 +} /* End namespace components */
  121 +} /* End namespace osdev */
  122 +
  123 +#endif /* OSDEV_COMPONENTS_ORMBATCHCHANGE_H */
src/ormhandler.cpp 0 → 100644
  1 +++ a/src/ormhandler.cpp
  1 +#include "ormhandler.h"
  2 +#include "dcxmlconfig.h"
  3 +#include "log.h"
  4 +#include "dbrelation.h"
  5 +#include "threadcontext.h"
  6 +
  7 +#include <QSqlRecord>
  8 +#include <QSqlField>
  9 +#include <QSqlRelation>
  10 +#include <QSqlIndex>
  11 +
  12 +#include <QtDebug>
  13 +
  14 +using namespace osdev::components;
  15 +
  16 +typedef QList<DbRelation*> Relations;
  17 +
  18 +OrmHandler::OrmHandler(QObject *_parent)
  19 + : QObject( _parent )
  20 + , m_qhOrmTables()
  21 + , m_dbConnection()
  22 + , m_className( "OrmHandler" )
  23 +{
  24 + m_dbConnection.setCredentials( DCXmlConfig::Instance().getDbCredentials() );
  25 +
  26 + // Check if a watchdog is required and connect those. If a database goes offline, all data should be redirected to the TransQueue.
  27 + if( DCXmlConfig::Instance().getEnableDbWatchDog() )
  28 + {
  29 + // Start the watchdog and set the interval.
  30 + m_dbConnection.startDbWatchDog( DCXmlConfig::Instance().getDbCheckInterval() );
  31 + }
  32 +}
  33 +
  34 +OrmHandler::~OrmHandler()
  35 +{
  36 +}
  37 +
  38 +void OrmHandler::start()
  39 +{
  40 + // connect the database.
  41 + LogInfo( m_className, "Connecting to database.." );
  42 + if( m_dbConnection.connectDatabase() )
  43 + {
  44 + // Read configuration
  45 + QStringList tableList = DCXmlConfig::Instance().getOrmTables();
  46 +
  47 + if( 0 == tableList.count() )
  48 + {
  49 + tableList = m_dbConnection.getTableNames();
  50 + }
  51 +
  52 + LogInfo( m_className,
  53 + QString("Connected to database %1") .arg( m_dbConnection.getDatabaseName() ) );
  54 +
  55 + // First make sure we have all tables created, before we create all relations...
  56 + for( const QString& tableName : tableList )
  57 + {
  58 + createOrmObject( tableName );
  59 + }
  60 + // -------------------------------------------------
  61 + // Create all relations on the same list of tables.
  62 + for( const QString& tableName : tableList )
  63 + {
  64 + QString status;
  65 + // If creation of the object succeeded it is safe to add the relations.
  66 + switch( createOrmRelation( tableName ) )
  67 + {
  68 + case OrmStatus::OrmStatusFailed:
  69 + status = "failed.";
  70 + break;
  71 + case OrmStatus::OrmStatusNoRelation:
  72 + status = "not created (As there are none.)";
  73 + break;
  74 + case OrmStatus::OrmStatusSuccess:
  75 + status = "succeeded.";
  76 + break;
  77 + }
  78 + LogInfo( "[OrmHandler::start]", QString( "Creation of ORM Relation for table : %1 %2" ).arg( tableName ).arg( status ) );
  79 + }
  80 + }
  81 + else
  82 + {
  83 + LogInfo( m_className,
  84 + QString( "Connection to database %1 failed with error : %2" )
  85 + .arg( m_dbConnection.getDatabaseName() )
  86 + .arg( m_dbConnection.getLastError() ) );
  87 +
  88 + //@todo : This will produce a SegmentationFault for now due to mutlihreaded pluginloading.
  89 + // This should be fixed in future releases by implementing a nice rolldown with locking
  90 + // mechanisms and shutdown events.
  91 + QMetaObject::invokeMethod( qApp, "quit", Qt::QueuedConnection );
  92 + }
  93 +}
  94 +
  95 +bool OrmHandler::createOrmObject( const QString& _table )
  96 +{
  97 + bool l_result = false;
  98 +
  99 + OrmTable *pModel = new OrmTable( m_dbConnection, this );
  100 + if ( pModel )
  101 + {
  102 + pModel->setTrackField( DCXmlConfig::Instance().getRecordTrackFieldName() );
  103 + pModel->setTable( m_dbConnection.quoteTableName( _table ) );
  104 + m_qhOrmTables.insert( _table, pModel );
  105 +
  106 + // cascade connect the rejectedsignal.
  107 + connect( pModel, &OrmTable::signalRejectedData, this, &OrmHandler::signalRejectedData );
  108 + l_result = true;
  109 + }
  110 +
  111 + return l_result;
  112 +}
  113 +
  114 +OrmHandler::OrmStatus OrmHandler::createOrmRelation( const QString& _table )
  115 +{
  116 + OrmStatus ormResult = OrmStatus::OrmStatusFailed;
  117 +
  118 + Relations lstRelations = m_dbConnection.getRelationByTableName( _table );
  119 + if( lstRelations.isEmpty() )
  120 + {
  121 + return OrmStatus::OrmStatusNoRelation;
  122 + }
  123 +
  124 + // Each relation found should be added to the table.
  125 + for( auto& relation : lstRelations )
  126 + {
  127 + // Save the relation to the table administration.
  128 + m_qhOrmTables.value( _table )->saveRelation( relation->constraintName(), relation );
  129 +
  130 + // Create a QSortProxyFilterModel and add to the table.
  131 + QSortFilterProxyModel *pModel = new QSortFilterProxyModel();
  132 +
  133 + pModel->setSourceModel( m_qhOrmTables.value( relation->foreignTable() ) );
  134 + m_qhOrmTables.value( _table )->setRelatedTable( relation->foreignTable(), pModel );
  135 + }
  136 +
  137 + // Check if the number of relations found is equal to the number of relations
  138 + // added to the ORM-table.
  139 + if( lstRelations.count() == m_qhOrmTables.value( _table )->numberOfRelations() )
  140 + {
  141 + ormResult = OrmStatus::OrmStatusSuccess;
  142 + }
  143 + else
  144 + {
  145 + LogDebug( "[OrmHandler::createOrmRelation]", QString( "Number of relations : %1 => Number of relations registered : %2" ).arg( lstRelations.count() ).arg( m_qhOrmTables.value( _table )->numberOfRelations() ) );
  146 + ormResult = OrmStatus::OrmStatusFailed;
  147 + }
  148 +
  149 + return ormResult;
  150 +}
  151 +
  152 +void OrmHandler::receiveData( const QSharedPointer<ORMRelData>& dataContainer )
  153 +{
  154 + ThreadContextScope tcs( dataContainer->traceId() );
  155 + LogDebug( "[OrmHandler::receiveData]", dataContainer->asString() );
  156 + // Check if we have an actual database running. *if* it died, redirect immediately
  157 + if( m_dbConnection.isOpen() )
  158 + {
  159 + if( m_qhOrmTables.contains( dataContainer->getMainTableName() ) )
  160 + {
  161 + LogDebug("[OrmHandler::receiveData]", QString("Table entry found for %1").arg( dataContainer->getMainTableName() ) );
  162 + m_qhOrmTables.value( dataContainer->getMainTableName() )->writeData( dataContainer );
  163 + }
  164 + else
  165 + {
  166 + LogWarning("[OrmHandler::receiveData]", QString("No table entry found for %1").arg( dataContainer->getMainTableName() ) );
  167 + emit signalRejectedData( dataContainer );
  168 + }
  169 + }
  170 + else
  171 + {
  172 + emit signalRejectedData( dataContainer );
  173 + }
  174 +}
  175 +
  176 +QString OrmHandler::getPrimaryKey( const QString& tableName ) const
  177 +{
  178 + if( m_qhOrmTables.contains( tableName ) )
  179 + {
  180 + return m_qhOrmTables.value( tableName )->primaryKey().name();
  181 + }
  182 + return QString();
  183 +}
src/ormhandler.h 0 → 100644
  1 +++ a/src/ormhandler.h
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +#ifndef OSDEV_COMPONENTS_ORMHANDLER_H
  23 +#define OSDEV_COMPONENTS_ORMHANDLER_H
  24 +
  25 +#include <memory>
  26 +
  27 +#include <QObject>
  28 +#include <QHash>
  29 +#include <QSharedPointer>
  30 +#include <QString>
  31 +
  32 +#include "dbconnector.h"
  33 +#include "ormtable.h"
  34 +#include "ormreldata.h"
  35 +
  36 +namespace osdev {
  37 +namespace components {
  38 +
  39 +/**
  40 + * @brief Handles the Object Relational Model
  41 + */
  42 +class OrmHandler : public QObject
  43 +{
  44 + Q_OBJECT
  45 +
  46 +public:
  47 + enum class OrmStatus
  48 + {
  49 + OrmStatusSuccess = 0,
  50 + OrmStatusNoRelation,
  51 + OrmStatusFailed
  52 + };
  53 +
  54 + /**
  55 + * @brief Constructor
  56 + * @param parent Parent QObject
  57 + */
  58 + explicit OrmHandler(QObject *parent = nullptr);
  59 +
  60 + /// Deleted copy-constructor
  61 + OrmHandler(const OrmHandler&) = delete;
  62 + /// Deleted assignment operator
  63 + OrmHandler& operator=(const OrmHandler&) = delete;
  64 + /// Deleted move-constructor
  65 + OrmHandler(const OrmHandler&&) = delete;
  66 + /// Deleted move operator
  67 + OrmHandler& operator=(const OrmHandler&&) = delete;
  68 +
  69 + virtual ~OrmHandler();
  70 +
  71 + /// @brief Start the ORM handler
  72 + void start();
  73 +
  74 +public slots:
  75 + /**
  76 + * @brief Receive data from an external source
  77 + * @param dataContainer - Data received Each layer has shared ownership of the
  78 + * pointer it receives. (See SSD @TODO Actualy SSD needs updating on this issue.)
  79 + */
  80 + void receiveData( const QSharedPointer<ORMRelData>& dataContainer );
  81 +
  82 + /*!
  83 + * \brief Gets the primary key from the given tableName
  84 + * \param tableName - The table we're interested in.
  85 + * \return The primary keyfield name.
  86 + */
  87 + QString getPrimaryKey( const QString& tableName ) const;
  88 +
  89 +signals:
  90 + /*!
  91 + * \brief Signal for RejectedData.
  92 + * \param dataContainer The data container for this signal.
  93 + */
  94 + void signalRejectedData( const QSharedPointer<ORMRelData>& dataContainer );
  95 +
  96 +private:
  97 + /**
  98 + * @brief Create an Object Relational Model object for a specific table
  99 + * @param _table Table name
  100 + * @return True if successful, false otherwise
  101 + */
  102 + bool createOrmObject( const QString& _table );
  103 +
  104 + /*!
  105 + * \brief Create an ORM object for a specific relation or constraint.
  106 + *
  107 + * \param table The name of the table for which to create an ORM relation.
  108 + * \return true if the object was created. False if something went wrong.
  109 + */
  110 + OrmStatus createOrmRelation( const QString& table );
  111 +
  112 + // Internal structures and objects.
  113 + QHash<QString, OrmTable*> m_qhOrmTables; ///< Maps table-name to TableModel*
  114 + DbConnector m_dbConnection; ///< Database connection
  115 + QString m_className; ///< Name of the class for logging purposes
  116 +};
  117 +
  118 +} // End namespace components
  119 +} // End namespace osdev
  120 +
  121 +#endif // OSDEV_COMPONENTS_ORMHANDLER_H
src/ormtable.cpp 0 → 100644
  1 +++ a/src/ormtable.cpp
  1 +#include "ormdata.h"
  2 +#include "ormtable.h"
  3 +#include "ormhandler.h"
  4 +#include "log.h"
  5 +#include "multisortfilterproxymodel.h"
  6 +
  7 +#include <QSqlError>
  8 +#include <QSqlRecord>
  9 +#include <QSqlField>
  10 +#include <QSqlQuery>
  11 +
  12 +#include <QtDebug>
  13 +#include <QtCore/qnamespace.h>
  14 +
  15 +using namespace osdev::components;
  16 +
  17 +OrmTable::OrmTable(const DbConnector &db, QObject *_parent)
  18 + : QSqlRelationalTableModel( _parent, db.getDatabase() )
  19 + , m_className( "OrmTable" )
  20 + , m_recTrackField()
  21 + , m_db( db )
  22 + , m_qhRelTables()
  23 + , m_qhRelations()
  24 + , m_qhFieldTypes()
  25 + , m_qhTimelines()
  26 +{
  27 + if ( !QSqlRelationalTableModel::database().isOpen() )
  28 + {
  29 + LogWarning( m_className, "Database is closed.");
  30 + }
  31 +}
  32 +
  33 +OrmTable::~OrmTable()
  34 +{
  35 +}
  36 +
  37 +void OrmTable::setTable( const QString& _table )
  38 +{
  39 + // Set tablename in the base class
  40 + QSqlRelationalTableModel::setTable( m_db.quoteTableName( _table ) );
  41 + m_className += "::" + this->tableName();
  42 +
  43 + // Make every change being written to the database.
  44 + QSqlRelationalTableModel::setEditStrategy( QSqlTableModel::OnRowChange );
  45 +
  46 + // Now we know the tablename, we can retrieve the fieldtypes by their name. To prevent a functioncall
  47 + // For each and every fieldname we're looking for, we store this in an internal Hash. Must be quicker than
  48 + // then accessing the database layer.
  49 + m_qhFieldTypes = m_db.getFieldNamesAndTypes( _table );
  50 +
  51 + QSqlRecord _headerData = QSqlRelationalTableModel::database().record( m_db.quoteTableName( _table ) );
  52 + for( int nCount = 0; nCount < _headerData.count(); nCount++ )
  53 + {
  54 + QSqlRelationalTableModel::setHeaderData( nCount, Qt::Horizontal, _headerData.fieldName(nCount) );
  55 + }
  56 +
  57 + // Now the table is set, headerinfo loaded and commit behaviour set :
  58 + // Open the table and read from the database.
  59 + QSqlRelationalTableModel::select();
  60 +}
  61 +
  62 +bool OrmTable::insertRecord( int _row, const QSqlRecord &_record )
  63 +{
  64 + if ( QSqlRelationalTableModel::database().isOpen() )
  65 + {
  66 + LogDebug( m_className, "Database is open." );
  67 + }
  68 + else
  69 + {
  70 + LogWarning( m_className, "Database is closed." );
  71 + }
  72 +
  73 + bool b_result = QSqlRelationalTableModel::insertRecord( _row, _record );
  74 +
  75 + if ( !b_result )
  76 + {
  77 + LogError( QString( "[OrmTable - %1]" ).arg( QSqlRelationalTableModel::tableName() ),
  78 + QString( "Database gave error : %1")
  79 + .arg( this->lastError().text() ) );
  80 + }
  81 +
  82 + return b_result;
  83 +}
  84 +
  85 +bool OrmTable::insertRecords(int row, const QList<QSqlRecord>& _records )
  86 +{
  87 + bool l_bResult = true;
  88 +
  89 + QSqlRelationalTableModel::setEditStrategy( QSqlTableModel::OnRowChange );
  90 + for( const QSqlRecord& _record : _records )
  91 + {
  92 + l_bResult &= insertRecord( row, _record );
  93 + }
  94 + QSqlRelationalTableModel::submitAll();
  95 +
  96 + // Reset the transaction method.
  97 + QSqlRelationalTableModel::setEditStrategy( QSqlTableModel::OnRowChange );
  98 +
  99 + return l_bResult;
  100 +}
  101 +
  102 +void OrmTable::writeData( const QSharedPointer<ORMRelData>& dataObject )
  103 +{
  104 + // Reset the filter for this table before doing anything else
  105 + this->resetFilter();
  106 +
  107 + // Relations can be set on different fields. We have to take in account those
  108 + // fields could be given in a list.
  109 + ORMData* pMainTable = this->solveRelations( dataObject );
  110 + if( nullptr == pMainTable || pMainTable->getContainerName().isEmpty() )
  111 + {
  112 + LogDebug( "[OrmTable::writeData]", QString( "No maintable defined. Exiting..." ) );
  113 + emit signalRejectedData( dataObject );
  114 + return;
  115 + }
  116 +
  117 + bool updateSuccess = false;
  118 + /// Here we check for the variable list. See if one of the variables is of type :
  119 + /// QVariant( QList<Qvariant) ) Instead of a normal update, we should use
  120 + /// a batched_update.
  121 + if( pMainTable->hasLists() )
  122 + {
  123 + /// @todo: This is kind of a special case. For now we assume there is an inclusive
  124 + /// and exclusive way of updating these records. All these updates should be done
  125 + /// in a single transaction. The 'not-mentioned records' should be de-coupled.
  126 + /// If those fail, the entire package should be rejected, including the ones that were
  127 + /// mentioned. Default behaviour is "Each - change - a - transaction" so we need some
  128 + /// extra precautions here.
  129 +
  130 + // Check if all items in the list exist in the database. If not the package is rejected.
  131 + if (!m_db.recordsExist(pMainTable->getContainerName(), pMainTable->getKeyField(), pMainTable->getValue( pMainTable->getKeyField() ).toList()))
  132 + {
  133 + LogInfo( "[OrmTable::writeData]", "Not all records are present. Package is rejected." );
  134 + emit signalRejectedData( dataObject );
  135 + return;
  136 + }
  137 +
  138 + // Set the TransactionScopeGuard. If it goes out-of-scope, for whatever reason,
  139 + // it will do a Rollback if a transaction was started.
  140 + OrmTransGuard l_scopeGuard;
  141 +
  142 + // Check to see if the database supports transactions
  143 + if( m_db.supportTransactions() )
  144 + {
  145 + if( !m_db.transactionBegin() )
  146 + {
  147 + LogDebug( "[OrmTable::writeData]", "Transaction failed to start." );
  148 + emit signalRejectedData( dataObject );
  149 + return;
  150 + }
  151 + l_scopeGuard = OrmTransGuard( m_db );
  152 + }
  153 +
  154 + std::unique_ptr<ORMData> mainTableCopy(pMainTable->clone());
  155 + updateSuccess = batchUpdate( mainTableCopy.get(), dataObject->getSourceId() );
  156 + if( !updateSuccess )
  157 + {
  158 + emit signalRejectedData( dataObject );
  159 + return;
  160 + }
  161 +
  162 + if( ( static_cast<int>(TableActions::actionMergeUpdate) == mainTableCopy->getDefaultAction() )
  163 + && ( !mainTableCopy->isMergeable() ) ) // If there is more than one key besides the keyfield, we cannot handle this (yet)
  164 + {
  165 + LogWarning( "[OrmTable::writeData]", QString( "Dataobject (ORMData) is not fit for mergeUpdate and is discarded : %1" ).arg( mainTableCopy->asString() ) );
  166 + return;
  167 + }
  168 +
  169 + if( ( static_cast<int>(TableActions::actionMergeUpdate) == mainTableCopy->getDefaultAction() )
  170 + && ( mainTableCopy->isMergeable() ) )
  171 + {
  172 + deCoupleRecords( mainTableCopy.get() );
  173 + }
  174 + else if (static_cast<int>(TableActions::actionUpdate) == mainTableCopy->getDefaultAction())
  175 + {
  176 + LogDebug( "[OrmTable::writeData]", QString( "Action is a \"normal\" update : %1" ).arg( mainTableCopy->asString() ) );
  177 + }
  178 +
  179 + if( !this->commit() )
  180 + {
  181 + emit signalRejectedData( dataObject );
  182 + }
  183 + auto timeline = m_qhTimelines[dataObject->getSourceId()];
  184 + if (timeline)
  185 + {
  186 + timeline->commit();
  187 + }
  188 + return;
  189 + }
  190 + else
  191 + {
  192 + if( static_cast<int>(TableActions::actionUpdate) == pMainTable->getDefaultAction() )
  193 + {
  194 + // ===================================================================================
  195 + // == S I N G L E R E C O R D U P D A T E ==
  196 + // ===================================================================================
  197 +
  198 + // See if we can actually update this record by checking its timestamp validity.
  199 + //
  200 + if( m_recTrackField.isEmpty() || m_recTrackField.isNull() )
  201 + {
  202 + updateSuccess = this->updateRecord( pMainTable );
  203 + }
  204 + else
  205 + {
  206 + if( this->isPackageValid( pMainTable ) )
  207 + {
  208 + pMainTable->setValue( m_recTrackField, QVariant( pMainTable->timeStamp() ) );
  209 + updateSuccess = this->updateRecord( pMainTable );
  210 + }
  211 + else
  212 + {
  213 + // Nope. Package is older. Discard.
  214 + LogInfo( "[OrmTable::writeData]", QString( "Discarding package because it is older than information in the database" ) );
  215 + return;
  216 + }
  217 + }
  218 + }
  219 +
  220 + if( updateSuccess )
  221 + {
  222 + return;
  223 + }
  224 + }
  225 +
  226 + // ===================================================================================
  227 + // == I N S E R T ==
  228 + // ===================================================================================
  229 + // If a timestamp is used in the database, add it to the table definition.
  230 + if( !m_recTrackField.isEmpty() || !m_recTrackField.isNull() )
  231 + {
  232 + pMainTable->setValue( m_recTrackField, pMainTable->timeStamp() );
  233 + }
  234 +
  235 +
  236 + // If we get to here, we're ready to create the new SQLRecord and add it to the table.
  237 + QSqlRecord newRecord = this->buildSqlRecord( pMainTable );
  238 + if( !newRecord.isEmpty() )
  239 + {
  240 + if( !m_db.createRecord( newRecord, this->tableName() ) )
  241 + {
  242 + // @todo : Change to a more database dependant, generic error checking based on keywords....
  243 + if( this->lastError().text().contains( "violates unique constraint" ) ||
  244 + this->lastError().text().contains( "violates not-null constraint" ) )
  245 + {
  246 + // Just drop the package and be done with it. This is a unique constraint violation
  247 + LogDebug( "[OrmTable::writeData]", QString( "There was and error creating the record : %1" ).arg( this->lastError().text() ) );
  248 + return;
  249 + }
  250 + else
  251 + {
  252 + LogDebug( "[OrmTable::writeData]", QString( "There was and error creating the record : %1" ).arg( this->lastError().text() ) );
  253 + LogDebug( "[OrmTable::writeData]", "Deferring the datapackage to the TransQueue." );
  254 + emit signalRejectedData( dataObject );
  255 + }
  256 +
  257 + }
  258 + else
  259 + {
  260 + return;
  261 + }
  262 + }
  263 + else
  264 + {
  265 + LogDebug( "[OrmTable::writeData]", QString( "Creating the new record failed.! : %1" ).arg( this->lastError().text() ) );
  266 + }
  267 +}
  268 +
  269 +bool OrmTable::updateRecord( ORMData* dataObject )
  270 +{
  271 +
  272 + auto lNumRecords = tableFilter( dataObject );
  273 + if( lNumRecords > 1 )
  274 + {
  275 + LogError( "[OrmTable::updateRecord]", QString( "Multiple records found..." ) );
  276 + return false;
  277 + }
  278 + else if( lNumRecords == 0 )
  279 + {
  280 + LogWarning( "[OrmTable::updateRecord]", QString( "No record found...Falling back to insert...." ) );
  281 + return false;
  282 + }
  283 + else
  284 + {
  285 + // All criteria are met.. Time to update the record.
  286 + // Each and and every field will be written, with the exception of the keyfield..
  287 + // First get a list of all field names.
  288 + bool updateSuccess = true;
  289 + QStringList fieldNames = dataObject->getFieldNames();
  290 + for( const QString& fieldName : fieldNames )
  291 + {
  292 + if( fieldName != dataObject->getKeyField() )
  293 + {
  294 + int colIndex = this->fieldIndex( fieldName );
  295 + if( -1 == colIndex )
  296 + {
  297 + LogError( "[OrmTable::updateRecord]", QString( "No index found for : %1" ).arg( fieldName ) );
  298 + }
  299 + else
  300 + {
  301 + LogDebug( "[OrmTable::updateRecord]", QString( "Updating field : %1 (Index : %2) with value : %3" )
  302 + .arg( fieldName )
  303 + .arg( colIndex )
  304 + .arg( dataObject->getValue( fieldName ).toString() ) );
  305 +
  306 + this->setData( this->index( 0, colIndex ),
  307 + ConversionUtils::convertToType(dataObject->getValue( fieldName ) ,m_qhFieldTypes.value( fieldName )),
  308 + Qt::EditRole );
  309 + }
  310 + }
  311 + else
  312 + {
  313 + LogDebug( "[OrmTable::updateRecord]", QString( "Skipping keyfield %1 ..." ).arg( fieldName ) );
  314 + }
  315 + }
  316 +
  317 + // Process all changes.
  318 + updateSuccess = this->submit();
  319 + if( !updateSuccess )
  320 + {
  321 + LogError( "[OrmTable::updateRecord]",
  322 + QString( "Record failed to update, Database gave error : %1").arg( this->lastError().text() ) );
  323 + }
  324 +
  325 + this->setFilter( "" );
  326 + this->select();
  327 + return updateSuccess;
  328 + }
  329 +}
  330 +
  331 +bool OrmTable::batchUpdate( ORMData* dataObject, const QUuid& sourceId )
  332 +{
  333 + // If MergedUpdate = true, we have to do an ex- and inclusive update.
  334 + // (Inclusive meaning everyting that is mentioned, Exclusive : everything that is not in range..)
  335 + // Similar to WHERE <field> NOT IN ( <VALUE1>, <Value2>, <VALUE..> )
  336 +
  337 + QString fieldName;
  338 +
  339 + // Get all fields to update, without the keyfield.
  340 + // We only support a single field update at this moment.
  341 + for( const auto& fn : dataObject->getFieldNames() )
  342 + {
  343 + if( fn != dataObject->getKeyField() )
  344 + {
  345 + fieldName = fn;
  346 + break;
  347 + }
  348 + }
  349 +
  350 + auto timeline = m_qhTimelines[sourceId];
  351 + if (!timeline)
  352 + {
  353 + auto pTimeline = QSharedPointer<Timeline>::create();
  354 + timeline.swap(pTimeline);
  355 + m_qhTimelines[sourceId] = timeline;
  356 + }
  357 + OrmBatchChange obc(Timestamp(dataObject->timeStamp()), dataObject->getValue( fieldName ), OrmBatchChange::toSet(dataObject->getValue( dataObject->getKeyField() ).toList()));
  358 + auto proposed = timeline->evaluate(obc, static_cast<int>(TableActions::actionMergeUpdate) == dataObject->getDefaultAction());
  359 +
  360 + if (!proposed.valid())
  361 + {
  362 + LogInfo( "[OrmTable::batchUpdate]", "Proposed update will not be executed" );
  363 + return false;
  364 + }
  365 + dataObject->setValue( dataObject->getKeyField(), OrmBatchChange::toList(proposed.changeSet()));
  366 + QString strTable = dataObject->getContainerName();
  367 + return m_db.associate(strTable, dataObject->getKeyField(), dataObject->getValue( dataObject->getKeyField() ).toList(), fieldName, dataObject->getValue( fieldName ));
  368 +}
  369 +
  370 +bool OrmTable::deCoupleRecords( ORMData* dataObject )
  371 +{
  372 + QString fieldName;
  373 +
  374 + // Get all fields to update, without the keyfield.
  375 + // We only support a single field update at this moment.
  376 + for( const auto& fn : dataObject->getFieldNames() )
  377 + {
  378 + if( fn != dataObject->getKeyField() )
  379 + {
  380 + fieldName = fn;
  381 + break;
  382 + }
  383 + }
  384 + QString strTable = dataObject->getContainerName();
  385 + return m_db.disassociate(strTable, dataObject->getKeyField(), dataObject->getValue( dataObject->getKeyField() ).toList(), fieldName, dataObject->getValue( fieldName ));
  386 +}
  387 +
  388 +QList<OrmTableRow> OrmTable::readData( const QString& fieldName, const QString& filterExp )
  389 +{
  390 + int columnIndex = this->fieldIndex( fieldName );
  391 + QSortFilterProxyModel filterModel;
  392 + filterModel.setSourceModel( this );
  393 +
  394 + if( columnIndex > -1 )
  395 + {
  396 +
  397 + filterModel.setFilterKeyColumn( columnIndex );
  398 + filterModel.setFilterRegExp( QRegExp( filterExp, Qt::CaseSensitive, QRegExp::Wildcard ) );
  399 + }
  400 +
  401 + if( filterModel.rowCount() > 0 )
  402 + {
  403 + // Looks like there was a result. Pack into a structure..
  404 + int rows = filterModel.rowCount();
  405 + int columns = filterModel.columnCount();
  406 +
  407 + // For each row and column, iterate the data structure..
  408 + for( int _rowCount = 0; _rowCount < rows; _rowCount++ )
  409 + {
  410 + qDebug() << "=____";
  411 + for( int _colCount = 0; _colCount < columns; _colCount++ )
  412 + {
  413 + qDebug() << filterModel.headerData( _colCount, Qt::Horizontal, Qt::DisplayRole ) << " : "
  414 + << filterModel.data( filterModel.index( _rowCount, _colCount ), Qt::DisplayRole );
  415 + }
  416 + }
  417 + }
  418 +
  419 + return QList<OrmTableRow>();
  420 +}
  421 +
  422 +QList<DbRelation *> OrmTable::getRelationsByTableName( const QString& _tableName )
  423 +{
  424 + QList<DbRelation*> lstResult;
  425 +
  426 + for( auto relName : m_qhRelations.keys() )
  427 + {
  428 + if( m_qhRelations.value( relName )->foreignTable() == _tableName )
  429 + {
  430 + lstResult.append( m_qhRelations.value( relName ) );
  431 + }
  432 + }
  433 +
  434 + return lstResult;
  435 +}
  436 +
  437 +QSqlRecord OrmTable::buildSqlRecord( ORMData* tableObject )
  438 +{
  439 + LogDebug( "[OrmTable::buildSqlRecord]", QString( "Building record with : %1 " ).arg( tableObject->asString() ) );
  440 + QStringList fieldNames = tableObject->getFieldNames();
  441 + QSqlRecord sqlRecord;
  442 + for( const QString& fieldName : fieldNames )
  443 + {
  444 + if( fieldName != "container" )
  445 + {
  446 + QSqlField sqlField( fieldName, m_qhFieldTypes.value( fieldName ) );
  447 + sqlField.setValue( ConversionUtils::convertToType( tableObject->getValue( fieldName ), // The value
  448 + m_qhFieldTypes.value( fieldName ) ) ); // The Type
  449 + sqlRecord.append( sqlField );
  450 + }
  451 + }
  452 +
  453 + return sqlRecord;
  454 +}
  455 +
  456 +void OrmTable::setRelatedTable(const QString& _tableName, QSortFilterProxyModel* pTable)
  457 +{
  458 + m_qhRelTables.insert( _tableName, pTable );
  459 +}
  460 +
  461 +QSortFilterProxyModel* OrmTable::getRelatedTable(const QString& _tableName) const
  462 +{
  463 + return m_qhRelTables.value( _tableName );
  464 +}
  465 +
  466 +
  467 +void OrmTable::saveRelation( const QString& relationName, DbRelation* pRelation )
  468 +{
  469 + LogDebug( "[OrmTable::saveRelation]", QString( "Saving Relation : %1" ).arg( relationName ) );
  470 + m_qhRelations.insert( relationName, pRelation );
  471 +}
  472 +
  473 +DbRelation* OrmTable::getRelation( const QString& relationName ) const
  474 +{
  475 + return m_qhRelations.value( relationName, nullptr );
  476 +}
  477 +
  478 +QStringList OrmTable::getRelationNames() const
  479 +{
  480 + return QStringList( m_qhRelations.keys() );
  481 +}
  482 +
  483 +ORMData* OrmTable::solveRelations( const QSharedPointer<ORMRelData>& dataObject )
  484 +{
  485 + // First we get the Maintable.... This will be our return object.
  486 + ORMData* pMainTable = dataObject->getMainTableContainer();
  487 + if( nullptr == pMainTable )
  488 + {
  489 + return nullptr;
  490 + }
  491 +
  492 + if( pMainTable->hasLists() )
  493 + {
  494 + LogWarning( "OrmTable::solveRelations", QString( "Maintable has Lists.... : %1" ).arg( pMainTable->asString() ) );
  495 + }
  496 +
  497 + QStringList constraintList = this->getRelationNames();
  498 +
  499 + for( auto constraint : constraintList )
  500 + {
  501 + const DbRelation* l_relation = QPointer<DbRelation>( this->getRelation( constraint ) );
  502 + QString foreignPrimKey;
  503 + QString foreignFieldName;
  504 + QVariant foreignFieldValue;
  505 +
  506 + LogDebug("[OrmTable::solveRelations]", QString("Get related table %1").arg(l_relation->foreignTable()));
  507 + ORMData* pTable = dataObject->getRelatedContainer( l_relation->foreignTable() );
  508 + if( nullptr != pTable )
  509 + {
  510 + // We assume there is only 1 relation between two tables
  511 + int numFields = pTable->getFieldCount();
  512 + if( 0 == numFields )
  513 + {
  514 + LogWarning( "[OrmTable::solveRelations]", QString( "No relations found to table : %1 " ).arg( l_relation->foreignTable() ) );
  515 + return nullptr;
  516 + }
  517 + else if( 1 == numFields )
  518 + {
  519 + foreignPrimKey = l_relation->foreignPrimKey();
  520 + foreignFieldName = pTable->getFieldNames().first();
  521 + // ===========================================================================================
  522 + // L I S T O R S I N G L E V A L U E R E L A T I O N
  523 + // ===========================================================================================
  524 + foreignFieldValue = pTable->getValue( foreignFieldName );
  525 + if( foreignFieldValue.isNull() )
  526 + {
  527 + // Some constraints are allowed to be absent. For now treat this constraint as such.
  528 + // If it is needed afterall it will prevent the write in a later stage
  529 + pMainTable->setValue( l_relation->indexColumn(), QVariant() );
  530 + continue;
  531 + }
  532 +
  533 + // Looks like we have all data collected. Retrieve the Primary key value needed.
  534 + // If foreignPrimKey is empty it will be retrieved from the database meta data.
  535 + QVariant l_primKeyValue = m_db.getRelationKey( l_relation->foreignTable(), foreignFieldName, foreignFieldValue, foreignPrimKey );
  536 + if( !l_primKeyValue.isNull() )
  537 + {
  538 + // We have a value of the IndexColumn. Complete the mainTable....
  539 + pMainTable->setValue( l_relation->indexColumn(), l_primKeyValue );
  540 + }
  541 + else
  542 + {
  543 + LogError( "[OrmTable::solveRelations]", QString( "No primary key found for table : %1 " ).arg( l_relation->foreignTable() ) );
  544 + return nullptr;
  545 + }
  546 + // ===========================================================================================
  547 + }
  548 + else
  549 + {
  550 + /// @todo : At this moment we don't handle multiple relations. Lotsplitting is requiring this, so it must be implemented as soon as
  551 + /// the requirements are defined.
  552 + LogError( "[OrmTable::solveRelations]", QString( "multiple relations found to table : %1 " ).arg( l_relation->foreignTable() ) );
  553 + return nullptr;
  554 + }
  555 + }
  556 + else
  557 + {
  558 + LogDebug( "[OrmTable::solveRelations]", QString( "Unresolved constraint %1. We assume all data is provided by the object.." ).arg( constraint ) );
  559 + }
  560 + }
  561 +
  562 + return pMainTable;
  563 +}
  564 +
  565 +int OrmTable::tableFilter( ORMData* dataObject )
  566 +{
  567 + int nResult = -1;
  568 +
  569 + // Check for a valid pointer.
  570 + if( nullptr == dataObject )
  571 + {
  572 + LogError( "[OrmTable::tableFilter]", QString( "No data structure given." ) );
  573 + return 0;
  574 + }
  575 +
  576 + // Check if update is the default action
  577 + if( static_cast<int>(TableActions::actionUpdate) != dataObject->getDefaultAction() )
  578 + {
  579 + LogWarning( "[OrmTable::tableFilter]", QString( "Update is not the default action for table : %1" ).arg( dataObject->getContainerName() ) );
  580 + return 0;
  581 + }
  582 +
  583 + /// @todo : Create an AND / OR filter.
  584 + // +----------------------------------+
  585 + // | Keyfield | KeyValue | TypeFilter |
  586 + // +----------+----------+------------+
  587 + // | = 1 | = 1 | equals |
  588 + // | = 1 | > 1 | OR | ( WHERE IN ( a, b, c, d...) )
  589 + // | > x | > y | AND | ( x equals y)
  590 + // | <> KV | <> KF | INVALID |
  591 + // +----------+----------+------------+
  592 + //
  593 + // Check if there is a record that matches our criteria.
  594 +
  595 + const auto keyFields = dataObject->getKeyFieldList();
  596 +
  597 + for (const auto& field : keyFields)
  598 + {
  599 + if( this->fieldIndex(field) == -1 )
  600 + {
  601 + LogWarning( "[OrmTable::tableFilter]", QString( "No columnIndex found for : %1" ).arg(field) );
  602 + return 0;
  603 + }
  604 + }
  605 +
  606 + QString strFilter;
  607 + const auto numberOfKeys = keyFields.length();
  608 +
  609 + if (0 == numberOfKeys)
  610 + {
  611 + LogWarning( "[OrmTable::tableFilter]", QString( "No keyField given for table : %1" ).arg( dataObject->getContainerName() ) );
  612 + return 0;
  613 + }
  614 + else if (1 == numberOfKeys)
  615 + {
  616 + const auto& keyField = *keyFields.begin();
  617 +
  618 + if( dataObject->getValue( keyField ).type() == QVariant::List ) // OR
  619 + {
  620 + QList<QVariant> tmpList = dataObject->getValue( keyField ).toList();
  621 + for( int lstCount = 0; lstCount < tmpList.count(); lstCount++ )
  622 + {
  623 + if( tmpList.at(lstCount).type() == QVariant::Int )
  624 + {
  625 + strFilter += keyField + "=" + tmpList.at( lstCount ).toString();
  626 + }
  627 + else
  628 + {
  629 + strFilter += "\"" + keyField + "\"='" + tmpList.at( lstCount ).toString().replace("{","").replace("}","") + "'";
  630 + }
  631 +
  632 + if( ( lstCount + 1 ) < tmpList.count() )
  633 + {
  634 + strFilter += " OR ";
  635 + }
  636 + }
  637 + }
  638 + else // equals
  639 + {
  640 + if( dataObject->getValue( keyField ).type() == QVariant::Int )
  641 + {
  642 + strFilter = "\"" + keyField + "\"="+ dataObject->getValue( keyField ).toString();
  643 + }
  644 + else
  645 + {
  646 + strFilter = "\"" + keyField + "\"='" + dataObject->getValue( keyField ).toString().replace("{","").replace("}","") + "'";
  647 + }
  648 + }
  649 + }
  650 + else // numberOfKeys > 1, AND
  651 + {
  652 + // =========================================================
  653 + // @todo: Setup a multiple fields filter. For now it is just a multiple values value filter.
  654 +
  655 + auto iter = keyFields.constBegin();
  656 + while (keyFields.constEnd() != iter)
  657 + {
  658 + if( dataObject->getValue( *iter ).type() == QVariant::Int )
  659 + {
  660 + strFilter += "\"" + *iter + "\"=" + dataObject->getValue( *iter ).toString();
  661 + }
  662 + else
  663 + {
  664 + strFilter += "\"" + *iter + "\"='" + dataObject->getValue( *iter ).toString().replace("{","").replace("}","") + "'";
  665 + }
  666 +
  667 + ++iter;
  668 +
  669 + if( keyFields.end() != iter )
  670 + {
  671 + strFilter += " AND ";
  672 + }
  673 + }
  674 + }
  675 +
  676 + this->setFilter( strFilter );
  677 + LogDebug( "[OrmTable::tableFilter]", QString( "Filter : %1" ).arg( this->filter() ) );
  678 + if( this->select() )
  679 + {
  680 + nResult = this->rowCount();
  681 + }
  682 + else
  683 + {
  684 + LogError( "[OrmTable::tableFilter]", QString( "There was a problem executing the query : %1" ).arg( this->selectStatement() ) );
  685 + }
  686 + // =========================================================
  687 + return nResult;
  688 +}
  689 +
  690 +int OrmTable::resetFilter()
  691 +{
  692 + this->setFilter( "" );
  693 + this->select();
  694 + return rowCount();
  695 +}
  696 +
  697 +bool OrmTable::isPackageValid( ORMData* dataObject )
  698 +{
  699 + if( nullptr == dataObject )
  700 + {
  701 + LogError( "[OrmTable::isPackageValid]", "No ORMData dataobject passed." );
  702 + return false;
  703 + }
  704 +
  705 + // Determine the field indices for the proxy filter
  706 + auto keyFields = dataObject->getKeyFieldList();
  707 + QList<qint32> fieldIndices;
  708 + auto iter = keyFields.begin();
  709 + while (keyFields.end() != iter)
  710 + {
  711 + auto idx = this->fieldIndex(*iter);
  712 + if (idx > -1)
  713 + {
  714 + fieldIndices.push_back(idx);
  715 + ++iter;
  716 + }
  717 + else
  718 + {
  719 + LogWarning("[OrmTable::isPackageValid]", QString("Keyfield \"%1\" is unknown. Not using it in filter.").arg(*iter));
  720 + iter = keyFields.erase(iter);
  721 + }
  722 + }
  723 +
  724 + // Create the filter model and set its filter criteria
  725 + MultiSortFilterProxyModel oModelFilter;
  726 + oModelFilter.setFilterKeyColumns( fieldIndices );
  727 +
  728 + // Build the filter expression.
  729 + for (qint32 i = 0; i < fieldIndices.size(); ++i)
  730 + {
  731 + oModelFilter.setFilterFixedString( fieldIndices[i], QString( "%1" ).arg( dataObject->getValue( keyFields[i] ).toString() ) );
  732 + }
  733 +
  734 + // Attach filter to this table model.
  735 + oModelFilter.setSourceModel( this );
  736 +
  737 + // If the number of record = 1, we seem to have found our record.
  738 + if( 0 == oModelFilter.rowCount() )
  739 + {
  740 + // Seems like we are good to go.
  741 + // This is a first timer...
  742 + LogDebug("[OrmTable::isPackageValid]", "No record found");
  743 + return true;
  744 + }
  745 + else if ( 1 == oModelFilter.rowCount() )
  746 + {
  747 + QModelIndex modIndex = oModelFilter.index( 0, this->fieldIndex( m_recTrackField ) );
  748 + unsigned long long tmp_timestamp = oModelFilter.data( modIndex, Qt::DisplayRole ).toULongLong();
  749 +
  750 + if( dataObject->timeStamp() > tmp_timestamp )
  751 + {
  752 + return true;
  753 + }
  754 + }
  755 + else
  756 + {
  757 + LogError( "[OrmTable::isPackageValid]", QString( "Multiple records found (%1) for %2 with value %3" )
  758 + .arg( oModelFilter.rowCount() )
  759 + .arg( dataObject->getKeyField() )
  760 + .arg( dataObject->getValue( dataObject->getKeyField() ).toString() ) );
  761 + }
  762 + return false;
  763 +}
  764 +
  765 +bool OrmTable::commit()
  766 +{
  767 + if( m_db.supportTransactions() )
  768 + {
  769 + if( m_db.transactionCommit() )
  770 + {
  771 + return true;
  772 + }
  773 + else
  774 + {
  775 + LogWarning( "[OrmTable::commit]", QString( "Transaction failed to commit with error : %1" )
  776 + .arg( m_db.getDatabase().driver()->lastError().text() ) );
  777 + }
  778 + }
  779 + else
  780 + {
  781 + if( m_db.getDatabase().driver()->lastError().driverText().isEmpty() )
  782 + {
  783 + return true;
  784 + }
  785 + else
  786 + {
  787 + LogDebug( "[OrmTable::commit]", QString( "Transaction not supported but last query failed with error : %1" )
  788 + .arg( m_db.getDatabase().driver()->lastError().driverText() ) );
  789 + }
  790 + }
  791 + return false;
  792 +}
src/ormtable.h 0 → 100644
  1 +++ a/src/ormtable.h
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +#ifndef OSDEV_COMPONENTS_ORMTABLE_H
  23 +#define OSDEV_COMPONENTS_ORMTABLE_H
  24 +
  25 +#include <QObject>
  26 +#include <QStringList>
  27 +#include <QSqlRelationalTableModel>
  28 +#include <QSortFilterProxyModel>
  29 +#include <QSqlDatabase>
  30 +#include <QSqlRecord>
  31 +#include <QHash>
  32 +
  33 +#include "dbconnector.h"
  34 +#include "ormreldata.h"
  35 +#include "ormtablerow.h"
  36 +#include "dbrelation.h"
  37 +#include "conversionutils.h"
  38 +#include "timeline.h"
  39 +
  40 +namespace osdev {
  41 +namespace components {
  42 +
  43 +/*!
  44 + * \brief This class is used as a TransActionsscopeGuard
  45 + * It takes a database connection at its constructor
  46 + * and calls the rollBack method in its destructor.
  47 + */
  48 +class OrmTransGuard
  49 +{
  50 +public:
  51 + OrmTransGuard()
  52 + : m_db(nullptr)
  53 + {
  54 + }
  55 +
  56 + explicit OrmTransGuard( DbConnector& _db )
  57 + : m_db( &_db )
  58 + {
  59 + }
  60 +
  61 + ~OrmTransGuard()
  62 + {
  63 + if (nullptr != m_db)
  64 + {
  65 + m_db->transactionRollback();
  66 + }
  67 + }
  68 +
  69 + OrmTransGuard(const OrmTransGuard&) = delete;
  70 + OrmTransGuard& operator=(const OrmTransGuard&) = delete;
  71 +
  72 + OrmTransGuard(OrmTransGuard&& rhs)
  73 + : m_db(std::move(rhs.m_db))
  74 + {
  75 + rhs.m_db = nullptr;
  76 + }
  77 +
  78 + OrmTransGuard& operator=(OrmTransGuard&& rhs)
  79 + {
  80 + if (this == &rhs)
  81 + {
  82 + return *this;
  83 + }
  84 +
  85 + if (nullptr != m_db)
  86 + {
  87 + m_db->transactionRollback();
  88 + }
  89 + m_db = std::move(rhs.m_db);
  90 + rhs.m_db = nullptr;
  91 + return *this;
  92 + }
  93 +
  94 +private:
  95 + DbConnector* m_db;
  96 +};
  97 +
  98 +/*!
  99 + * \brief The OrmTable represents a database table. Depending
  100 + * on the EditStrategy, it updates the database by each
  101 + * inserted record or after all records are inserted.
  102 + * \note Internally setTable is used. Please do not use setQuery
  103 + * also because that will invalidate the OrmTable meta data.
  104 + */
  105 +class OrmTable : public QSqlRelationalTableModel
  106 +{
  107 + Q_OBJECT
  108 +
  109 +public:
  110 + /*!
  111 + * \brief Default constructor.
  112 + * \param parent The parent object as a QObject pointer
  113 + * \param db The database layer we're connected through.
  114 + */
  115 + explicit OrmTable(const DbConnector& db, QObject *parent = nullptr );
  116 +
  117 + // Deleted copy- and move constructors
  118 + OrmTable( const OrmTable& ) = delete;
  119 + OrmTable( const OrmTable&& ) = delete;
  120 + OrmTable& operator=( const OrmTable& ) = delete;
  121 + OrmTable& operator=( const OrmTable&& ) = delete;
  122 +
  123 + /// \brief Destructor
  124 + virtual ~OrmTable();
  125 +
  126 + /*!
  127 + * \brief Sets the tablename, this model represents.
  128 + * \param table The name of the table.
  129 + */
  130 + void setTable( const QString& table );
  131 +
  132 + /*!
  133 + * \brief Insert a single QSqlRecord into the model.
  134 + * \param row Inserts the record at position row.
  135 + * If row is negative, the record will be appended to the end.
  136 + * \param record Record to insert
  137 + * \return true if the record could be inserted, otherwise false.
  138 + */
  139 + bool insertRecord( int row, const QSqlRecord &record );
  140 +
  141 + /*!
  142 + * \brief Insert multiple QSqlRecords into the model.
  143 + * \param row Inserts the records at position row.
  144 + * If row is negative, the records will be appended to the end.
  145 + * calls insertRecord internally.
  146 + * \param records Records to insert
  147 + * \return true if the record could be inserted, otherwise false.
  148 + */
  149 + bool insertRecords(int row, const QList<QSqlRecord>& records );
  150 +
  151 + /*!
  152 + * \brief writes the data given in the dataobject to the table and to the database.
  153 + * \param dataObject - Contains all relational data.
  154 + */
  155 + void writeData( const QSharedPointer<ORMRelData>& dataObject );
  156 +
  157 + /*!
  158 + * \brief writes the values to a given record, identified by the fieldname in "KeyField".
  159 + * \param dataObject - Contains "flat" data
  160 + * \return True if successful, false if not.
  161 + */
  162 + bool updateRecord( ORMData* dataObject );
  163 +
  164 + /*!
  165 + * \brief This method is for convenience only. It will retrieve (a set of) records, based on the criteria given.
  166 + * \param fieldName - The fieldname we like to filter on
  167 + * \param filterExp - The filter expression
  168 + * \return A QList of SQLRecords containing the results.
  169 + * The list will be empty if no records were found or the table is empty.
  170 + */
  171 + QList<OrmTableRow> readData( const QString& fieldName = QString(), const QString& filterExp = QString() );
  172 +
  173 +
  174 + /*!
  175 + * \brief Returns the number of relations added to this table.
  176 + * \return The number of relations. 0 if none.
  177 + */
  178 + int numberOfRelations() const { return m_qhRelations.count(); }
  179 +
  180 + /*!
  181 + * \brief setRelatedTable
  182 + * \param tableName The name of the related table.
  183 + * \todo Verify pointer ownership
  184 + * \param pTable Owning pointer to the Table. Ownership is transferred to this instance.
  185 + */
  186 + void setRelatedTable( const QString& tableName, QSortFilterProxyModel* pTable );
  187 +
  188 + /*!
  189 + * \brief Gets the RelatedTable for the specified table name.
  190 + * \param tableName The name of the table for which to get the relation.
  191 + * \return The relation to the table with the specified table name.
  192 + */
  193 + QSortFilterProxyModel* getRelatedTable( const QString& tableName ) const;
  194 +
  195 + /*!
  196 + * \brief Store the relation to another table, stored by its unique name.
  197 + * ORMTable is owner of the relation pointer.
  198 + * \param relationName - the Name of the relation as known by the database.
  199 + */
  200 + void saveRelation( const QString& relationName, DbRelation* pRelation );
  201 +
  202 + /*!
  203 + * \brief Get the relationstructure
  204 + * \param relationName The name of the relation to get.
  205 + * \return The pointer to the datastructure. NULL if none exist.
  206 + */
  207 + DbRelation* getRelation( const QString& relationName ) const;
  208 +
  209 + /*!
  210 + * \brief Get all registered constraintnames by their true name.
  211 + * \return The list of all relation by their contraint names. If no relations exist, the list will be empty.
  212 + */
  213 + QStringList getRelationNames() const;
  214 +
  215 + /*!
  216 + * \brief Gets the attached relation based on a given tablename.
  217 + * \param tableName - The tablename we want the relation on.
  218 + * \return The relation we're searching for. isValid is true if there is a relation, false if not.
  219 + */
  220 + QList<DbRelation*> getRelationsByTableName( const QString& tableName );
  221 +
  222 + /*!
  223 + * \brief This method will solve any relations in the structure.
  224 + * \param dataObject - The ORMRelData object describing the data
  225 + * \return The ORMData object with all solved references, if there were any.
  226 + * If the main container can't be retrieved, it will return a nullptr. Check for validity.
  227 + */
  228 + ORMData* solveRelations( const QSharedPointer<ORMRelData>& dataObject );
  229 +
  230 + /*!
  231 + * \brief Do a "batched" update. The dataobject contains lists which represents different records.
  232 + * \param dataObject - The "maintable" object after resolving the relations.
  233 + * It is the responsibility of this TableObject to delete this object if successful,
  234 + * else it will be rejected and ownership will be transferred to the next subsystem.
  235 + * before calling this method, a transaction should be started on the referenced database
  236 + * connection. If this method ( and possible following statements like deCoupleRecords )
  237 + * the transaction should be committed.
  238 + * \param sourceId The source from which the dataObject originates.
  239 + * \return True if the complete update was successful. False if not and should result in a rejected
  240 + * transaction. ( See signalRejectedData() ) Rejecting should be done by the calling method.
  241 + */
  242 + bool batchUpdate( ORMData* dataObject, const QUuid& sourceId );
  243 +
  244 + /*!
  245 + * \brief Decouple records from their previous value (Normally a foreign key relation).
  246 + * This method will fail if a field in the database is not-nullable or the number
  247 + * of fields is larger than 2 (1 searchField and 1 updateable field.
  248 + * \param dataObject - The dataobject containing all the information needed to update the record(s).
  249 + * It is the responsibility of this TableObject to delete this object if successful,
  250 + * else it will be rejected and ownership will be transferred to the next subsystem.
  251 + * before calling this method, a transaction should be started on the referenced database
  252 + * connection. If this method ( and possible previous statements like batchUpdate() )
  253 + * the transaction should be committed.
  254 + * \return true if successful. false if not.
  255 + */
  256 + bool deCoupleRecords( ORMData* dataObject );
  257 +
  258 + /*!
  259 + * \brief Get the name of the field that will track the mutation date / time in uSecs since EPOCH.
  260 + * \return The name of the field. Empty if no field was configured.
  261 + */
  262 + QString trackField() { return m_recTrackField; }
  263 +
  264 + /*!
  265 + * \brief Get the name of the field that will track the mutation date / time in uSecs since EPOCH.
  266 + * \param _recTrackFieldName - The name of the field, coming from the configuration.
  267 + */
  268 + void setTrackField( const QString& _recTrackFieldName ) { m_recTrackField = _recTrackFieldName; }
  269 +
  270 +
  271 +signals:
  272 + /*!
  273 + * \brief Signal for RejectedData.
  274 + * \param dataContainer The data container for this signal.
  275 + */
  276 + void signalRejectedData( const QSharedPointer<ORMRelData>& dataContainer );
  277 +
  278 +private:
  279 +
  280 + /*!
  281 + * \brief Translates an ORMData into an QSqlRecord
  282 + * \param tableObject - The tableObject we like to translate into an sql-record to store.
  283 + * \return The QSqlRecord object representing an entry. If the creation of the record fails,
  284 + * the result will be an empty SqlRecord structure.
  285 + */
  286 + QSqlRecord buildSqlRecord( ORMData* tableObject );
  287 +
  288 + /*!
  289 + * \brief Builds a query and filters the table data. Data isn't removed, but a resultset is shown.
  290 + *
  291 + * \param dataObject - The dataobject of this particular table as extracted from ORMRelData. Based on the
  292 + * keyField(s) and values, the filter is set. ( See also resetFilter() )
  293 + * \return The number of resulting records that match the criteria. If the filter fails, the result will be -1.
  294 + */
  295 + int tableFilter( ORMData* dataObject );
  296 +
  297 + /*!
  298 + * \brief Checks if the ORMData package is valid against the record it tries to update. Each table should have a
  299 + * field of type INT which contains the timestamp since epoch. Before updating a record, it should check if the timestamp
  300 + * of the package is actually newer than the timestamp of that record. If the package is newer ( a.k.a. Package timestamp > timestamp_record )
  301 + * it is valid to update the record with its values, including the timestamp. This will prevent the situation where records will be overwritten
  302 + * with "old" data. This method works together with the internal variable 'm_recTrackField'. Before calling this method, we have to check if this
  303 + * variable isn't empty. If so, we don't keep track of record changes in a timeline and we can update the records anyway.
  304 + *
  305 + * \param dataObject - The ORMData object containing a complete transaction.
  306 + * Does not have to be resolved by relations yet, this will be done after the validity check.
  307 + *
  308 + * \return True if the record can be updated, False if the package is older than the actual record.
  309 + */
  310 + bool isPackageValid( ORMData* dataObject );
  311 +
  312 + /*!
  313 + * \brief resets the filter, allowing the table to show all records. ( See also tableFilter() )
  314 + * \return The number of records show by the table.
  315 + */
  316 + int resetFilter();
  317 +
  318 + /*!
  319 + * \brief Commit a database transaction if one is in progress.
  320 + * \note If the driver does not support transactions this method will look at the last driver error and return false if it contains a message.
  321 + * \return True if commit succeeds or if there was no transaction in progress, false if commit fails.
  322 + */
  323 + bool commit();
  324 +
  325 + QString m_className; ///< Class name for logging purposes
  326 + QString m_recTrackField; ///< Name of the field, which keep tracks of changes on each record. If Empty, we don't track.
  327 + DbConnector m_db; ///< Database layer we're connected through. We need the reference to access its helper methods.
  328 + QHash<QString, QSortFilterProxyModel*> m_qhRelTables; ///< Filtered view on a related table.
  329 + QHash<QString, DbRelation*> m_qhRelations; ///< The actual relation Meta information stored for reference.
  330 + QHash<QString, QVariant::Type> m_qhFieldTypes; ///< The internal storage for all fieldtypes by their fieldname.
  331 + QHash<QUuid, QSharedPointer<Timeline>> m_qhTimelines; ///< Timelines used for the batch update
  332 +};
  333 +
  334 +} // End namespace components
  335 +} // End namespace osdev
  336 +
  337 +#endif /* OSDEV_COMPONENTS_ORMTABLE_H */
src/ormtablerow.cpp 0 → 100644
  1 +++ a/src/ormtablerow.cpp
  1 +#include "ormtablerow.h"
  2 +
  3 +using namespace osdev::components;
  4 +
  5 +OrmTableRow::OrmTableRow()
  6 + : m_stoData()
  7 +{
  8 +
  9 +}
  10 +
  11 +OrmTableRow::~OrmTableRow()
  12 +{
  13 + m_stoData.clear();
  14 +}
  15 +
  16 +void OrmTableRow::setField( QVariant vData, int column )
  17 +{
  18 + if( -1 == column || column >= m_stoData.count() )
  19 + {
  20 + m_stoData.append( vData );
  21 + }
  22 + else if( -1 < column && column < m_stoData.count() )
  23 + {
  24 + m_stoData.replace( column, vData );
  25 + }
  26 +}
  27 +
  28 +QVariant OrmTableRow::getField( int column )
  29 +{
  30 + if( -1 < column && column < m_stoData.count() )
  31 + {
  32 + return m_stoData.at( column );
  33 + }
  34 + return QVariant();
  35 +}
  36 +
  37 +void OrmTableRow::setFields( QList<QVariant> values )
  38 +{
  39 + // Replace the entire internal structure with the new list.
  40 + m_stoData.clear();
  41 + for( const QVariant& value : values )
  42 + {
  43 + m_stoData.append( value );
  44 + }
  45 +}
src/ormtablerow.h 0 → 100644
  1 +++ a/src/ormtablerow.h
  1 +#ifndef OSDEV_COMPONENTS_ORMTABLEROW_H
  2 +#define OSDEV_COMPONENTS_ORMTABLEROW_H
  3 +
  4 +#include <QVariant>
  5 +#include <QList>
  6 +
  7 +namespace osdev {
  8 +namespace components {
  9 +
  10 +/*!
  11 + * \brief This class represents a single record, completely decoupled.
  12 + * No fieldnames and no constraints.
  13 + */
  14 +class OrmTableRow
  15 +{
  16 +public:
  17 + OrmTableRow();
  18 + virtual ~OrmTableRow();
  19 +
  20 + void setField( QVariant vData, int column = -1 );
  21 + QVariant getField( int column );
  22 +
  23 + int getNumFields();
  24 + void setFields( QList<QVariant> values );
  25 +
  26 +private:
  27 + QList<QVariant> m_stoData;
  28 +};
  29 +
  30 +} /* End namespace components */
  31 +} /* End namespace osdev */
  32 +
  33 +#endif /* OSDEV_COMPONENTS_ORMTABLEROW_H */
src/ormthread.cpp 0 → 100644
  1 +++ a/src/ormthread.cpp
  1 +#include "ormthread.h"
  2 +#include "ormhandler.h"
  3 +#include "log.h"
  4 +
  5 +using namespace osdev::components;
  6 +
  7 +ORMThread::ORMThread()
  8 +{
  9 +}
  10 +
  11 +ORMThread::~ORMThread()
  12 +{
  13 +}
  14 +
  15 +void ORMThread::run()
  16 +{
  17 + OrmHandler *p_OrmHandler = new OrmHandler();
  18 +
  19 + // Connect incoming data
  20 + connect( this, &ORMThread::signalSendData, p_OrmHandler, &OrmHandler::receiveData );
  21 + // Cascade connect the rejectedData
  22 + connect( p_OrmHandler, &OrmHandler::signalRejectedData, this, &ORMThread::signalRejectedData );
  23 +
  24 + p_OrmHandler->start();
  25 +
  26 + this->exec();
  27 +}
  28 +
  29 +void ORMThread::dataToThread( const QSharedPointer<ORMRelData>& data )
  30 +{
  31 + QCoreApplication::processEvents();
  32 + LogDebug( "[ORMThread::dataToThread]", QString( "Data received for container : %1" )
  33 + .arg( data->getMainTableName() ) );
  34 + LogDebug( "[ORMThread::dataToThread]", data->asString() );
  35 +
  36 + emit signalSendData( data );
  37 + QCoreApplication::processEvents();
  38 +}
src/ormthread.h 0 → 100644
  1 +++ a/src/ormthread.h
  1 +#ifndef OSDEV_COMPONENTS_ORMTHREAD_H
  2 +#define OSDEV_COMPONENTS_ORMTHREAD_H
  3 +
  4 +#include <QThread>
  5 +#include <QSharedPointer>
  6 +
  7 +#include "ormreldata.h"
  8 +
  9 +/* ______________________________________
  10 + * / Good day to deal with people in high \
  11 + * | places; particularly lonely |
  12 + * \ stewardesses. /
  13 + * --------------------------------------
  14 + * \
  15 + * \
  16 + * .--.
  17 + * |o_o |
  18 + * |:_/ |
  19 + * // \ \
  20 + * (| | )
  21 + * /'\_ _/`\
  22 + * \___)=(___/
  23 + *//*!
  24 + * \brief This class *manages* the thread, the ORM layer will be started in.
  25 + * Derived from QThread it isn't the thread itself but implements the
  26 + * 'run()' method that'll start the thread. (QThread isn't meant to be
  27 + * instantiated. It has to be derived.. :) )
  28 + *
  29 + * And sometimes it can be a Stewardess. That's polymorphism for yah!
  30 + */
  31 +namespace osdev {
  32 +namespace components {
  33 +
  34 +class ORMThread : public QThread
  35 +{
  36 + Q_OBJECT
  37 +
  38 +public:
  39 + ORMThread();
  40 +
  41 + // Deleted copy-constructor
  42 + ORMThread( const ORMThread& ) = delete;
  43 + ORMThread( const ORMThread&& ) = delete;
  44 + ORMThread& operator=( const ORMThread& ) = delete;
  45 + ORMThread& operator=( const ORMThread&& ) = delete;
  46 +
  47 + /*!
  48 + * \brief Destructor
  49 + */
  50 + virtual ~ORMThread();
  51 +
  52 + /*!
  53 + * \brief Override from QThread. This will actually create and start the thread.
  54 + * Objects used here are "moved" to the new thread and run
  55 + */
  56 + void run() override;
  57 +
  58 + /*!
  59 + * \brief Send the data to the intended thread.
  60 + * \param data Data coming from the plugin. Ownership is moved to the thread and that
  61 + * specific thread is responsible for cleaning up.
  62 + */
  63 + void dataToThread( const QSharedPointer<ORMRelData>& data );
  64 +
  65 +signals:
  66 + /*!
  67 + * \brief Used to send the datastructure to the thread.
  68 + * \param data Data coming from the plugin.
  69 + */
  70 + void signalSendData( const QSharedPointer<ORMRelData>& data );
  71 +
  72 + /*!
  73 + * \brief If the thread is unable to process the data that was sent, it will "reject" this.
  74 + * Normally it would then be send to a subsystem, capable of storing the data and
  75 + * resend it for re-processing.
  76 + * \param data Data originally comming from the plugin (it can be enriched along the way).
  77 + */
  78 + void signalRejectedData( const QSharedPointer<ORMRelData>& data );
  79 +
  80 +};
  81 +
  82 +
  83 +} /* End namespace components */
  84 +} /* End namespace osdev */
  85 +
  86 +#endif /* OSDEV_COMPONENTS_ORMTHREAD_H */
src/ormtransqueue.cpp 0 → 100644
  1 +++ a/src/ormtransqueue.cpp
  1 +#include "ormtransqueue.h"
  2 +
  3 +#include <QCoreApplication>
  4 +#include "log.h"
  5 +
  6 +namespace osdev {
  7 +namespace caelus {
  8 +
  9 +OrmTransQueue::OrmTransQueue( QObject *_parent )
  10 + : QObject(_parent)
  11 + , m_pQueueTimer( nullptr )
  12 + , m_pQueueMutex( nullptr )
  13 + , m_ormQueue()
  14 +{
  15 + m_pQueueMutex = new QMutex( QMutex::NonRecursive );
  16 + setTimeOut( 1000 );
  17 +}
  18 +
  19 +void OrmTransQueue::setTimeOut( int mseconds )
  20 +{
  21 + if( nullptr == m_pQueueTimer )
  22 + {
  23 + m_pQueueTimer = new QTimer( this );
  24 + m_pQueueTimer->setSingleShot( false );
  25 +
  26 + // Connect signal / slot..
  27 + connect( m_pQueueTimer, SIGNAL( timeout() ),
  28 + this, SLOT( slotProcessQueue() ), Qt::QueuedConnection );
  29 + }
  30 +
  31 + // Set the interval in milliseconds and start.
  32 + m_pQueueTimer->setInterval( mseconds );
  33 +}
  34 +
  35 +bool OrmTransQueue::processing() const
  36 +{
  37 + return m_pQueueTimer->isActive();
  38 +}
  39 +
  40 +void OrmTransQueue::startProcessing( bool force )
  41 +{
  42 + if( force )
  43 + {
  44 + m_pQueueTimer->stop();
  45 + this->slotProcessQueue();
  46 + }
  47 + else
  48 + {
  49 + if( !m_pQueueTimer->isActive() )
  50 + {
  51 + m_pQueueTimer->start();
  52 + }
  53 + }
  54 +}
  55 +
  56 +void OrmTransQueue::stopProcessing( bool force )
  57 +{
  58 + if( force && !m_pQueueTimer->isActive() )
  59 + {
  60 + m_pQueueTimer->stop();
  61 + }
  62 +}
  63 +
  64 +bool OrmTransQueue::setTransaction( ORMRelData *pData )
  65 +{
  66 + QMutexLocker mutLock( m_pQueueMutex ); // << Later on we should do a conditional wait
  67 + LogDebug( "[OrmTransQueue::setTransaction]", "Adding transaction to the transaction queue : ");
  68 + m_ormQueue.enqueue( pData );
  69 + LogDebug("[OrmTransQueue::setTransaction]", QString("Number of transactions in the queue : %1").arg( m_ormQueue.size() ) );
  70 + this->startProcessing();
  71 +
  72 + return true; // << Rethink about it..
  73 +}
  74 +
  75 +void OrmTransQueue::slotProcessQueue()
  76 +{
  77 + m_pQueueTimer->stop();
  78 + LogInfo( "[OrmTransQueue::slotProcessQueue]", "Start processing the transaction queue." );
  79 + if( m_pQueueMutex->tryLock() )
  80 + {
  81 + LogDebug( "[OrmTransQueue::slotProcessQueue]", "!!!!!!!!!!!!!!!!!!!!! Lock Acquired !!!!!!!!!!!!!!!" );
  82 + if( !m_ormQueue.isEmpty() )
  83 + {
  84 + LogInfo( "[OrmTransQueue::slotProcessQueue]", QString( "{ Before Emit } Number of transactions in the queue : %1 ").arg( m_ormQueue.size() ) );
  85 + ORMRelData *pData = m_ormQueue.dequeue();
  86 + Q_UNUSED( pData );
  87 + LogDebug( "[OrmTransQueue::slotProcessQueue]", QString("{ Before Emit, after dequeue} Number of transactions in the queue : %1 ").arg( m_ormQueue.size() ) );
  88 + // emit signalProcessData( pData );
  89 + LogDebug( "[OrmTransQueue::slotProcessQueue]", QString("{ After Emit, after dequeue} Number of transactions in the queue : %1 ").arg( m_ormQueue.size() ) );
  90 + QCoreApplication::processEvents();
  91 + }
  92 + m_pQueueMutex->unlock();
  93 + LogDebug( "[OrmTransQueue::slotProcessQueue]", "!!!!!!!!!!!!!!!!!!!!! Lock Freed !!!!!!!!!!!!!!!" );
  94 + }
  95 + else
  96 + {
  97 + LogDebug( "[OrmTransQueue::slotProcessQueue]", "!!!!!!!!!!!!!!!!!!!!! No Lock !!!!!!!!!!!!!!!" );
  98 + }
  99 +
  100 + LogInfo( "[OrmTransQueue::slotProcessQueue]", "Done processing the transaction queue." );
  101 + if( m_ormQueue.size() > 0 )
  102 + {
  103 + m_pQueueTimer->start();
  104 + }
  105 +}
  106 +
  107 +
  108 +
  109 +} /* End namespace caelus */
  110 +} /* End namespace osdev */
src/ormtransqueue.h 0 → 100644
  1 +++ a/src/ormtransqueue.h
  1 +#ifndef OSDEV_CAELUS_ORMTRANSQUEUE_H
  2 +#define OSDEV_CAELUS_ORMTRANSQUEUE_H
  3 +
  4 +// Qt
  5 +#include <QObject>
  6 +#include <QTimer>
  7 +#include <QMutex>
  8 +#include <QMutexLocker>
  9 +#include <QQueue>
  10 +
  11 +// Local
  12 +#include "ormreldata.h"
  13 +
  14 +namespace osdev {
  15 +namespace caelus {
  16 +
  17 +class OrmTransQueue : public QObject
  18 +{
  19 + Q_OBJECT
  20 +
  21 +public:
  22 + explicit OrmTransQueue( QObject *_parent = 0 );
  23 +
  24 + // Copy constructors.
  25 +
  26 + // Deleted copy- and move constructors
  27 + OrmTransQueue( const OrmTransQueue& ) = delete;
  28 + OrmTransQueue( const OrmTransQueue&& ) = delete;
  29 + OrmTransQueue& operator=( const OrmTransQueue& ) = delete;
  30 + OrmTransQueue& operator=( const OrmTransQueue&& ) = delete;
  31 +
  32 + // Timer control.
  33 + /*!
  34 + * \brief setTimeOut
  35 + * \param msec
  36 + */
  37 + void setTimeOut(int mseconds = 1 );
  38 +
  39 + // Queue control
  40 + /*!
  41 + * \brief Inserts a ORMReldata objectpointer into the queue.
  42 + * \param pData - the (valid) pointer of the ORMRelData structure we want to queue for later processing
  43 + * \return True if transaction was saved to the queue successfully, false if not.
  44 + */
  45 + bool setTransaction( ORMRelData *pData );
  46 +
  47 + bool processing() const;
  48 +
  49 + void startProcessing( bool force = false );
  50 +
  51 + void stopProcessing( bool force = false );
  52 +
  53 + /*!
  54 + * \brief Returns the number of transactions stored in the queue.
  55 + * \return The number of transactions, or 0 if empty.
  56 + */
  57 + int transactions_count() { return m_ormQueue.size(); }
  58 +
  59 +signals:
  60 + void signalProcessData( ORMRelData *pData );
  61 +
  62 +private:
  63 + QTimer *m_pQueueTimer; ///< Timer controlling the processing of the queue.
  64 + QMutex *m_pQueueMutex; ///< Mutex to prevent race conditions.
  65 + QQueue<ORMRelData*> m_ormQueue; ///< The actual FIFO taking a ORMRelData pointer as input.
  66 +
  67 +private slots:
  68 + /*!
  69 + * \brief slotProcessQueue
  70 + */
  71 + void slotProcessQueue();
  72 +
  73 +
  74 +};
  75 +
  76 +} /* End namespace caelus */
  77 +} /* End namespace osdev */
  78 +
  79 +#endif /* OSDEV_CAELUS_ORMTRANSQUEUE_H */
src/timeline.cpp 0 → 100644
  1 +++ a/src/timeline.cpp
  1 +#include "timeline.h"
  2 +
  3 +#include "log.h"
  4 +
  5 +using namespace osdev::components;
  6 +
  7 +Timeline::Timeline()
  8 + : m_timeline(10)
  9 + , m_proposedChange()
  10 +{
  11 +}
  12 +
  13 +const OrmBatchChange& Timeline::evaluate(const OrmBatchChange& desiredChange, bool exclusiveUpdate)
  14 +{
  15 + m_proposedChange = OrmBatchChange {};
  16 + if (m_timeline.full() && desiredChange < m_timeline.front())
  17 + {
  18 + LogWarning("[Timeline::evaluate]", QString("Incoming change (%1) is older then the oldest registered change (%2).").arg(desiredChange.timestamp().toString()).arg(m_timeline.front().timestamp().toString()));
  19 + return m_proposedChange; // change is older then anything we know, discard.
  20 + }
  21 +
  22 + m_proposedChange = desiredChange;
  23 + for (const auto& ch : m_timeline)
  24 + {
  25 + if (!m_proposedChange.processChange(ch, exclusiveUpdate))
  26 + {
  27 + break;
  28 + }
  29 + }
  30 + if ( m_proposedChange.valid()
  31 + && !m_timeline.empty()
  32 + && m_proposedChange < m_timeline.back())
  33 + {
  34 + // reset the timestamp so that the proposed change will be the latest in the timeline when committed.
  35 + auto ts = m_timeline.back().timestamp();
  36 + m_proposedChange.setTimestamp(++ts);
  37 + }
  38 + return m_proposedChange;
  39 +}
  40 +
  41 +void Timeline::commit()
  42 +{
  43 + if (m_proposedChange.valid())
  44 + {
  45 + m_timeline.push_back(m_proposedChange);
  46 + m_proposedChange = OrmBatchChange {};
  47 + }
  48 +}
  49 +
src/timeline.h 0 → 100644
  1 +++ a/src/timeline.h
  1 +#ifndef OSDEV_COMPONENTS_TIMELINE_H
  2 +#define OSDEV_COMPONENTS_TIMELINE_H
  3 +
  4 +// boost
  5 +#include <boost/circular_buffer.hpp>
  6 +
  7 +#include "ormbatchchange.h"
  8 +
  9 +namespace osdev {
  10 +namespace components {
  11 +
  12 +/**
  13 + * @brief The timeline contains the changes that where made to a specific Batch/Merge update table.
  14 + * Only a small number of changes is recorded. A change arrives that is older then the oldest recorded
  15 + * change this change will not be considered.
  16 + */
  17 +class Timeline
  18 +{
  19 +public:
  20 + /**
  21 + * @brief Creates an empty timeline.
  22 + */
  23 + Timeline();
  24 +
  25 + // Non copyable, non movable.
  26 + Timeline(const Timeline&) = delete;
  27 + Timeline& operator=(const Timeline&) = delete;
  28 + Timeline(Timeline&&) = delete;
  29 + Timeline& operator=(Timeline&&) = delete;
  30 +
  31 + /**
  32 + * @brief Evaluate an incoming change against the already registered changes.
  33 + * A previously evaluated change is discarded.
  34 + * @param desiredChange The incoming change.
  35 + * @param exclusiveUpdate Flag that indicates if this change should be evaluated in an exclusive manner.
  36 + * @return The reshaped and possibly invalidated change that is left.
  37 + * @note The evaluated change is cached so that it can be commited if the database update actually succeeds.
  38 + */
  39 + const OrmBatchChange& evaluate(const OrmBatchChange& desiredChange, bool exclusiveUpdate);
  40 +
  41 + /**
  42 + * @brief Commit an evaluated change.
  43 + * Only valid proposals are commited. This method should be called when the change
  44 + * is sucessfuly written to the database.
  45 + * @note To make this somewhat trustworthy the underlying database should support transactions.
  46 + */
  47 + void commit();
  48 +
  49 +private:
  50 + boost::circular_buffer<OrmBatchChange> m_timeline; ///< Only a small number of changes are buffered.
  51 + OrmBatchChange m_proposedChange; ///< Cache the evaluated proposed change for commiting.
  52 +};
  53 +
  54 +} /* End namespace components */
  55 +} /* End namespace osdev */
  56 +
  57 +#endif /* OSDEV_COMPONENTS_TIMELINE_H */
src/timestamp.cpp 0 → 100644
  1 +++ a/src/timestamp.cpp
  1 +/* ****************************************************************************
  2 + * Copyright 2019 Open Systems Development BV *
  3 + * *
  4 + * Permission is hereby granted, free of charge, to any person obtaining a *
  5 + * copy of this software and associated documentation files (the "Software"), *
  6 + * to deal in the Software without restriction, including without limitation *
  7 + * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
  8 + * and/or sell copies of the Software, and to permit persons to whom the *
  9 + * Software is furnished to do so, subject to the following conditions: *
  10 + * *
  11 + * The above copyright notice and this permission notice shall be included in *
  12 + * all copies or substantial portions of the Software. *
  13 + * *
  14 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
  15 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
  16 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
  17 + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
  18 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
  19 + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
  20 + * DEALINGS IN THE SOFTWARE. *
  21 + * ***************************************************************************/
  22 +#include "timestamp.h"
  23 +
  24 +using namespace osdev::components;
  25 +
  26 +Timestamp::Timestamp()
  27 + : m_msecsSinceEpoch(0)
  28 + , m_seqNr(0)
  29 +{
  30 +}
  31 +
  32 +Timestamp::Timestamp(unsigned long long msecsSinceEpoch)
  33 + : m_msecsSinceEpoch(msecsSinceEpoch)
  34 + , m_seqNr(1)
  35 +{
  36 +}
  37 +
  38 +bool Timestamp::operator<(const Timestamp& rhs) const
  39 +{
  40 + if (m_msecsSinceEpoch < rhs.m_msecsSinceEpoch)
  41 + {
  42 + return true;
  43 + }
  44 +
  45 + if (m_msecsSinceEpoch == rhs.m_msecsSinceEpoch)
  46 + {
  47 + return m_seqNr < rhs.m_seqNr;
  48 + }
  49 + return false;
  50 +}
  51 +
  52 +Timestamp& Timestamp::operator++() // prefix
  53 +{
  54 + if (!valid())
  55 + {
  56 + return *this; // an invalid timestamp can only be made valid by assigning a valid timestamp to it.
  57 + }
  58 +
  59 + if (m_seqNr < std::numeric_limits<decltype(m_seqNr)>::max())
  60 + {
  61 + ++m_seqNr;
  62 + }
  63 + else
  64 + {
  65 + // The idea is to never reach this point by choosing a large datatype.
  66 + // Overflowing is not an option because then the < relation doesn't hold anymore.
  67 + throw std::out_of_range("sequence number overflow");
  68 + }
  69 + return *this;
  70 +}
  71 +
  72 +Timestamp Timestamp::operator++(int) // postfix
  73 +{
  74 + auto cpy(*this);
  75 + ++(*this);
  76 + return cpy;
  77 +}
  78 +
  79 +QString Timestamp::toString() const
  80 +{
  81 + return QString("%1:%2").arg(m_msecsSinceEpoch).arg(m_seqNr);
  82 +}
  83 +
src/timestamp.h 0 → 100644
  1 +++ a/src/timestamp.h
  1 +#ifndef OSDEV_COMPONENTS_TIMESTAMP_H
  2 +#define OSDEV_COMPONENTS_TIMESTAMP_H
  3 +
  4 +#include <limits>
  5 +#include <stdexcept>
  6 +
  7 +#include <QString>
  8 +
  9 +namespace osdev {
  10 +namespace components {
  11 +
  12 +/**
  13 + * @brief Timestamp class that allows formation of a sequence of ordered events.
  14 + */
  15 +class Timestamp
  16 +{
  17 +public:
  18 + /**
  19 + * @brief Creates an invalid timestamp. This is the oldest possible timestamp.
  20 + * @note A timestamp can only become valid by assigning a valid timestamp to it.
  21 + */
  22 + Timestamp();
  23 +
  24 + /**
  25 + * @brief Creates a timestamp based on miliseconds since the epoch.
  26 + * @param msecsSinceEpoch Number of miliseconds since the epoch. Value 0 leads to valid timestamp.
  27 + * @note Timestamp with value 0 is larger then the invalid timestamp.
  28 + */
  29 + explicit Timestamp(unsigned long long msecsSinceEpoch);
  30 +
  31 +
  32 + // default constructable and movable.
  33 + Timestamp(const Timestamp&) = default;
  34 + Timestamp& operator=(const Timestamp&) = default;
  35 + Timestamp(Timestamp&&) = default;
  36 + Timestamp& operator=(Timestamp&&) = default;
  37 +
  38 + /**
  39 + * @return true if this timestamp is valid, false otherwise.
  40 + */
  41 + bool valid() const
  42 + {
  43 + return (m_seqNr > 0);
  44 + }
  45 +
  46 + /**
  47 + * @return true if this timestamp is smaller (older) then rhs.
  48 + */
  49 + bool operator<(const Timestamp& rhs) const;
  50 +
  51 + /**
  52 + * @brief Prefix addition operator.
  53 + * Increases the Timestamp sequencenumber so that timestamps with the same number of msecs since the epoch can also be ordered.
  54 + * @note An invalid timestamp remains invalid.
  55 + * @return Reference to this timestamp.
  56 + * @throw std::out_of_range exception when seqnr is about to overflow.
  57 + */
  58 + Timestamp& operator++();
  59 +
  60 + /**
  61 + * @brief Postfix addition operator.
  62 + * Increases the Timestamp sequencenumber so that timestamps with the same number of msecs since the epoch can also be ordered.
  63 + * @note An invalid timestamp remains invalid.
  64 + * @return The timestamp as it was before increment.
  65 + * @throw std::out_of_range exception when seqnr is about to overflow.
  66 + */
  67 + Timestamp operator++(int);
  68 +
  69 + /**
  70 + * @return Stringified timestamp as msecsSinceEpoch:seqNr.
  71 + */
  72 + QString toString() const;
  73 +
  74 +private:
  75 + unsigned long long m_msecsSinceEpoch; ///< Number of miliseconds since the epoch.
  76 + unsigned long long m_seqNr; ///< Sequence number.
  77 +};
  78 +
  79 +} /* End namespace components */
  80 +} /* End namespace osdev */
  81 +
  82 +#endif /* OSDEV_COMPONENTS_TIMESTAMP_H */
tests/CMakeLists.txt 0 → 100644
  1 +++ a/tests/CMakeLists.txt
  1 +cmake_minimum_required(VERSION 3.0)
  2 +LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake)
  3 +
  4 +include(projectheader)
  5 +project_header(test_logutils)
  6 +
  7 +include_directories( SYSTEM
  8 + ${CMAKE_CURRENT_SOURCE_DIR}/../../src
  9 +)
  10 +
  11 +include(compiler)
  12 +set(SRC_LIST
  13 +)
  14 +
  15 +# add_executable( ${PROJECT_NAME}
  16 +# ${SRC_LIST}
  17 +# )
  18 +
  19 +# target_link_libraries(
  20 +# ${PROJECT_NAME}
  21 +# )
  22 +
  23 +# set_target_properties( ${PROJECT_NAME} PROPERTIES
  24 +# RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
  25 +# LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
  26 +# ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive
  27 +# )
  28 +
  29 +# include(installation)
  30 +# install_application()