#ifndef CRUDMODEL_H #define CRUDMODEL_H #include "ASyncDBConnection.h" #include "Pgsql_Connection.h" #include "IntegerRange.h" #include "ResultSourceMapping.h" #include "catalog/PgClass.h" #include "catalog/PgConstraint.h" #include "catalog/PgAttribute.h" #include #include #include #include #include #include #include class PgAttribute; 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); /// Show an arbitrary query result, cells are editable where the result maps them to a table with a primary key void setResult(std::shared_ptr db, std::shared_ptr result); 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(); /// True when there are edits that have not been saved to the database bool hasPendingChanges() const { return !m_pendingRowList.m_rows.empty(); } /// Writes all pending rows to the database, rows that fail stay pending bool saveChanges(); /// Discards all pending edits, the grid shows the last saved values again void revertChanges(); std::tuple removeRows(const std::set> &row_ranges); bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; signals: /// Emitted when the model goes from clean to having pending changes or back void pendingChangesChanged(bool hasChanges); private: /// Number of grid columns in front of the result columns (the row number column) static constexpr 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"; return res; } }; 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; 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_mapping; ASyncDBConnection m_dbConn; bool callLoadData = false; bool m_wholeTable = false; ///< true when showing a whole table (CRUD), false for an arbitrary query result std::shared_ptr m_roData; std::vector> m_columnList; // attribute per column in m_roData, empty for expression columns 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 as shown in the grid, pending edits take precedence over saved data Value getLatestData(int column, int row) const; Value getLatestData(const QModelIndex &index) const { return getLatestData(index.column() - PreColumnCount, index.row()); } /// Value as last loaded from or saved to the database Value getSavedData(const RowMapping &row_mapping, int columnIndex) const; /// Mapping of the row with the given key, nullptr when the row is not in the grid (anymore) RowMapping* findRowMapping(int rowKey); /// Call after changing the pending rows to emit pendingChangesChanged when the state toggled void updatePendingState(bool was_dirty); Oid getType(int column) const; // ---- Saving, all of these only use the data of the pending row and the row mapping bool savePendingChanges(); /// Inserts the row when it is new otherwise updates it. Returns the values of the row as stored in the database. std::tuple> saveRow(const PendingRow &pending_row, const RowMapping &row_mapping); std::tuple> saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping); /// Get the PKey values from when the row was last restored or retrieved. Pgsql::Params getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const; std::tuple createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping); std::tuple createInsertQuery(const PendingRow &pending_row); QString createDeleteStatement() const; /// Opens a separate connection for writing so edits don't interfere with the loading connection void connectForUpdate(Pgsql::Connection &connection) const; /// Tells the user about a failed write, the one place where the model shows a dialog void reportError(const QString &message) const; static std::vector rowValues(const Pgsql::Row &row); // ---- Rows void appendNewRow(); void appendNewRowInternal(); int lastRowKey = -1; int allocNewRowKey() { return ++lastRowKey; } /// Deletes the rows from the database in one transaction, throws on failure void deleteRowsFromDatabase(const std::set> &row_ranges); /// Removes the rows from the grid and drops their pending edits void removeRangesFromModel(const std::set> &row_ranges); void removeRangeFromModel(IntegerRange row_range); // ---- Table and column information /// Name of the table with the given oid, quoted and schema qualified QString tableName(Oid table_oid) const; /// Catalog name of the attribute behind a result column, falls back to the result column name QString attributeName(int col) const; bool columnIsReadOnly(int column_index) const; void initializeColumnList(); private slots: void loadIntoModel(std::shared_ptr data); void connectionStateChanged(); }; #endif // CRUDMODEL_H