pgLab/pglab/CrudModel.h
eelke 15bee33076 Made step to remove ASyncWindow in favour of usage of Future and FutureWatcher.
This should allow concurrency in the plugins to be independent from their container.

Contains also some work on the system for registering plugins.
2018-12-30 15:46:15 +01:00

327 lines
9.5 KiB
C++

#ifndef CRUDMODEL_H
#define CRUDMODEL_H
#include <QAbstractTableModel>
#include "ASyncDBConnection.h"
#include "Pgsql_Connection.h"
#include "IntegerRange.h"
#include "catalog/PgClass.h"
#include "catalog/PgConstraint.h"
#include "Pgsql_Connection.h"
#include <map>
#include <memory>
#include <set>
#include <tuple>
#include <vector>
#include <optional>
class PgConstraint;
class OpenDatabase;
/**
* @brief The CrudModel class
*
* Features
* - order by one or more user selectable columns
* - user filter condition
* - user limit and offset
* - hide columns (will not be retrieved can greatly speed up things when you disable wide columns)
* - can use foreign keys to display dropdown
*
*
* Need to keep track of changes from the original. Things to track
* - new data being entered for existing row
* - new data for new row
* - current data of modified row
* - current data of new row
* - which rows are deleted
* We need to be able to:
* - determine row count easily
* - find specific row from table easily
* - support resorting (without requerying)
* //- support custom filtering (without requerying)
*
* Keep data as much in original result
* - use redirect list to go from index to actual row in the result
* - use an additional map index by original row number containing changed data
* - might be beneficial to have vector<bool> in sync with original list to signal changes from the result
* this could prevent doing a lot of slower searches in the changed row set by only doing this when the direty markes
* has been set vector<bool> takes 1 bit per value making it very efficient and fast. We also could put a bit in the mapping
* vector but this probably would double the size of that list taking 32 times the size of vector<bool>.
*
*/
class CrudModel: public QAbstractTableModel {
Q_OBJECT
public:
explicit CrudModel(QObject *parent = nullptr);
~CrudModel() override;
void setConfig(std::shared_ptr<OpenDatabase> db, const PgClass &table);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
virtual Qt::ItemFlags flags(const QModelIndex &) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override;
void loadData();
std::tuple<bool, QString> removeRows(const std::set<IntegerRange<int>> &row_ranges);
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
public slots:
virtual bool submit() override;
virtual void revert() override;
private:
using PKeyValues = std::vector<std::string>;
class ColumnSort {
public:
enum Direction { Ascending, Descending };
enum NullSorting {
Default, ///< Behaves like NULL values are larger then non NULL values ASC NULLS LAST or DESC NULLS FIRST
First,
Last };
std::string columnName;
Direction direction = Direction::Ascending;
NullSorting nulls = NullSorting::Default;
std::string toSql() const
{
std::string res = columnName;
if (direction == Direction::Descending)
res += " DESC";
if (nulls == NullSorting::First)
res += " NULLS FIRST";
else if (nulls == NullSorting::Last)
res += " NULLS LAST";
}
};
// using RowData = std::vector<std::string>;
// using RowDataPtr = std::unique_ptr<RowData>;
// /** Choosen for a vector of pointers while vector is relatively slow with insertion and removal in the middle
// * I hope the small size of the elements will make this not much of an issue while the simple linear and sequential
// * nature of a vector will keep memory allocation fairly efficient.
// */
// using RowList = std::vector<RowDataPtr>;
// std::shared_ptr<RowList> resultToRowList(std::shared_ptr<Pgsql::Result> result);
using Value = std::optional<std::string>;
/** Used to remember the changes that have been made to rows.
*
* For simplicity it also holds the values for columns that have not been changed.
*/
class ModifiedRow {
public:
ModifiedRow() = default;
ModifiedRow(int row, const std::vector<Value> &values)
: m_row(row), m_values(values)
{}
ModifiedRow(int row, const std::vector<Value> &&values)
: m_row(row), m_values(values)
{}
const auto& data() const { return m_values; }
int row() const { return m_row; }
private:
int m_row = -1;
std::vector<Value> m_values;
};
using ModifiedRowList = std::map<int, ModifiedRow>;
/** Similar to a modified row but it only stores values for columns that actually have been edited.
*
*/
class PendingRow {
public:
using ValueMap = std::map<int, Value>;
explicit PendingRow(int row)
: m_row(row)
{}
const auto& data() const { return m_values; }
int row() const { return m_row; }
void setValue(int col, const Value &val)
{
m_values.insert_or_assign(col, val);
}
private:
int m_row;
ValueMap m_values;
};
class PendingRowList {
public:
using Map = std::map<int, PendingRow>;
PendingRow& getRow(int row)
{
auto iter = m_rows.lower_bound(row);
if (iter != m_rows.end() && iter->first == row) {
return iter->second;
}
else {
return m_rows.insert(iter, {row, PendingRow(row)})->second;
}
}
void setValue(int col, int row, const Value &value)
{
auto iter = m_rows.find(row);
if (iter == m_rows.end()) {
iter = m_rows.insert({row, PendingRow(row)}).first;
}
iter->second.setValue(col, value);
}
std::optional<Value> getValue(int col, int row) const
{
auto iter = m_rows.find(row);
if (iter != m_rows.end()) {
auto &r = iter->second;
auto cell = r.data().find(col);
if (cell != r.data().end())
return cell->second;
}
return std::nullopt;
}
auto begin() { return m_rows.begin(); }
auto end() { return m_rows.end(); }
void clear() { m_rows.clear(); }
Map m_rows;
};
class RowMapping {
public:
bool modified = false; ///< Row has been modified, look there for actual data
bool pending = false; ///< Row has pending changes, look first in pending for current data
int rowKey = -1; ///< value to use as key/index into the data lists
RowMapping() = default;
explicit RowMapping(int row_key)
: rowKey(row_key)
{}
};
using RowMappingVector = std::vector<RowMapping>;
std::shared_ptr<OpenDatabase> m_database;
std::optional<PgClass> m_table;
std::optional<PgConstraint> m_primaryKey;
ASyncDBConnection m_dbConn;
bool callLoadData = false;
std::shared_ptr<Pgsql::Result> m_roData;
/// \brief Total number of rows to show.
///
/// The first n rows are the rows in m_roData.
/// While a row is being edited the modifications are in the PendingRowList
/// When the edits have been submitted the new values are put in the ModifiedRowList
///
/// The last row is for creating new rows. There is no data for this row or it is
/// in the PendingRowList. As soon as the row has been inserted a new last empty row
/// will be created by increasing this variable. And the newly stored row will be in
/// the modified list.
///
int m_rowCount = 0;
PendingRowList m_pendingRowList;
/// \brief Maintains a list of all modified rows.
///
/// The key values are the indexes of the row before any rows were deleted.
///
ModifiedRowList m_modifiedRowList;
/// Keeps track of mapping grid rows to data rows
RowMappingVector m_rowMapping;
/// call on initial load to fill in the mappings
void initRowMapping();
// using RedirectVec = std::vector<int>;
// /// In sync with the actual table, used to efficiently find the correct row in the result
// RedirectVec m_redirectVector;
void loadIntoModel(std::shared_ptr<Pgsql::Result> data);
Value getData(const QModelIndex &index) const;
Oid getType(int column) const;
///
/// \brief columnName
/// \param col
/// \return
///
QString columnName(int col) const;
///
/// \brief getModifiedRow searches for the specified row in the modified set
/// \param row
/// \return Pointer to the modified element or nullptr if there is no modification
///
const ModifiedRow* getModifiedRow(int row) const;
///
/// \brief getPKeyForRow retrieve the primary key of the specified row
///
/// This function should not be called when there is no primarykey. (Editing should be disabled anyway in that case)
///
/// \param row Actual result row not intermeddiat model row
/// \return The values of the primary key column. If this is a new row it will return an empty list
///
PKeyValues getPKeyForRow(int row) const;
Pgsql::Params getPKeyParamsForRow(int row) const;
bool savePendingChanges();
std::tuple<QString, Pgsql::Params> createUpdateQuery(const PKeyValues &pkey_values, const PendingRow &pending_row);
std::tuple<QString, Pgsql::Params> createInsertQuery(const PendingRow &pending_row);
std::tuple<QString, Pgsql::Params> createDeleteStatement(const PKeyValues &pkey_values);
QString createDeleteStatement() const;
std::tuple<bool, ModifiedRow> updateRow(const PendingRow &pending_row);
void appendNewRow();
int lastRowKey = 0;
int allocNewRowKey() { return ++lastRowKey; }
/// Convert an attnum from the database catalog to the corresponding column in the model
///
/// \todo still assumes columns are in order, all being shown and no special column like oid shown.
int attNumToCol(int attnum) const { return attnum - 1; }
private slots:
void connectionStateChanged(ASyncDBConnection::State state);
// void queryResult(std::shared_ptr<Pgsql::Result> result);
// void queryError();
// void dataProcessingFutureFinished();
};
#endif // CRUDMODEL_H