#ifndef CRUDMODEL_H #define CRUDMODEL_H #include "ASyncDBConnection.h" #include "Pgsql_Connection.h" #include "IntegerRange.h" #include "catalog/PgClass.h" #include "catalog/PgConstraint.h" #include #include #include #include #include #include #include class PgConstraint; class OpenDatabase; class CrudModel: public QAbstractTableModel { Q_OBJECT public: explicit CrudModel(QObject *parent = nullptr); ~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(); std::tuple removeRows(const std::set> &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; const int PreColumnCount = 1; 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 Value = std::optional; /** 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 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, std::vector data) : rowKey(row_key) , modifiedRow(std::move(data)) {} RowMapping(int row_key) : rowKey(row_key) {} bool isModified() const { return !modifiedRow.empty(); } Value modifiedValue(int columnIndex) const { return modifiedRow[columnIndex]; } void setModifiedRowData(std::vector data) { modifiedRow = std::move(data); } bool isNew() const { // row in m_roData, thus it is not new if (modifiedRow.empty()) return false; // if all elements are empty then this is a new row for (auto &c : modifiedRow) if (c.has_value()) return false; return true; } private: std::vector modifiedRow; }; using RowMappingVector = std::vector; std::shared_ptr m_database; std::optional m_table; std::optional m_primaryKey; ASyncDBConnection m_dbConn; bool callLoadData = false; std::shared_ptr m_roData; PendingRowList m_pendingRowList; /// Keeps track of mapping grid rows to data rows RowMappingVector m_rowMapping; /// call on initial load to fill in the mappings void initRowMapping(); Value getLatestData(int column, int row) const; Value getLatestData(const QModelIndex &index) const { return getLatestData(index.column() - PreColumnCount, index.row()); } Value getSavedData(int columnIndex, int rowIndex) const; Value getSavedData(const RowMapping &row_mapping, int columnIndex) const; Oid getType(int column) const; QString columnName(int col) const; /// Get the PKey values from when the row was last restored or retrieved. Pgsql::Params getPKeyParamsForRow(int row) const; bool savePendingChanges(); std::tuple createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row); std::tuple createInsertQuery(const PendingRow &pending_row); std::tuple createDeleteStatement(const PKeyValues &pkey_values); QString createDeleteStatement() const; std::tuple> saveRow(const PendingRow &pending_row); void appendNewRow(); int lastRowKey = -1; 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; } void RemoveRangesOfRowsFromModel(const std::set> &row_ranges); void RemoveRangeOfRowsFromModel(IntegerRange row_range); bool IsLastRow(RowMappingVector::iterator mapping_iter) const; private slots: void loadIntoModel(std::shared_ptr data); void connectionStateChanged(ASyncDBConnection::StateData state); }; #endif // CRUDMODEL_H