#ifndef CRUDMODEL_H #define CRUDMODEL_H #include #include "ASyncDBConnection.h" #include "Pgsql_Connection.h" #include "PgClass.h" #include "PgConstraint.h" #include "Pgsql_Connection.h" #include #include #include #include #include class PgConstraint; class OpenDatabase; class ASyncWindow; /** * @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 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 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. * */ class CrudModel: public QAbstractTableModel { Q_OBJECT public: explicit CrudModel(ASyncWindow *async_win); ~CrudModel() override; void setConfig(std::shared_ptr 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(); bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; /// Removes selected rows void removeRows(); public slots: virtual bool submit() override; virtual void revert() override; private: using PKeyValues = std::vector; 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; // using RowDataPtr = std::unique_ptr; // /** 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; // std::shared_ptr resultToRowList(std::shared_ptr result); using Value = std::optional; /** 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 &values) : m_row(row), m_values(values) {} ModifiedRow(int row, const std::vector &&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 m_values; }; using ModifiedRowList = std::map; /** Similar to a modified row but it only stores values for columns that actually have been edited. * */ class PendingRow { public: using ValueMap = std::map; 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; 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 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; ASyncWindow * m_asyncWindow; std::shared_ptr m_database; std::optional m_table; std::optional m_primaryKey; ASyncDBConnection m_dbConn; bool callLoadData = false; std::shared_ptr 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; // /// In sync with the actual table, used to efficiently find the correct row in the result // RedirectVec m_redirectVector; void loadIntoModel(std::shared_ptr 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 createUpdateQuery(const PKeyValues &pkey_values, const PendingRow &pending_row); std::tuple createInsertQuery(const PendingRow &pending_row); std::tuple createDeleteStatement(const PKeyValues &pkey_values); QString createDeleteStatement() const; std::tuple 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 result); // void queryError(); // void dataProcessingFutureFinished(); }; #endif // CRUDMODEL_H