diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index 8367f32..8a8ce00 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -4,6 +4,7 @@ #include "catalog/PgAttribute.h" #include "catalog/PgAttributeContainer.h" #include "catalog/PgConstraintContainer.h" +#include "catalog/PgClassContainer.h" #include "SqlFormattingUtils.h" #include "Pgsql_oids.h" #include "CustomDataRole.h" @@ -39,12 +40,23 @@ void CrudModel::setConfig(std::shared_ptr db, const PgClass &table { m_database = db; m_table = table; - m_primaryKey = db->catalog()->constraints()->getPrimaryForRelation(table.oid()); + m_wholeTable = true; callLoadData = true; auto dbconfig = m_database->config(); m_dbConn.setupConnection(dbconfig); } +void CrudModel::setResult(std::shared_ptr db, std::shared_ptr result) +{ + m_database = db; + m_table.reset(); + m_wholeTable = false; + callLoadData = false; + auto dbconfig = m_database->config(); + m_dbConn.setupConnection(dbconfig); + loadIntoModel(result); +} + QVariant CrudModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section == 0) @@ -232,9 +244,11 @@ void CrudModel::loadIntoModel(std::shared_ptr data) m_pendingRowList.clear(); m_roData = data; initializeColumnList(); + m_mapping.emplace(createResultSourceMapping(*m_roData, *m_database->catalog())); lastRowKey = data->rows() - 1; initRowMapping(); - appendNewRowInternal(); + if (m_wholeTable) + appendNewRowInternal(); endResetModel(); } @@ -277,7 +291,7 @@ Qt::ItemFlags CrudModel::flags(const QModelIndex &index) const if (index.column() < PreColumnCount) return flags; - if (m_primaryKey && !columnIsReadOnly(index.column())) + if (!columnIsReadOnly(index.column())) flags |= Qt::ItemIsEditable; return flags; @@ -312,22 +326,17 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol return false; } -std::tuple> CrudModel::saveRow(const PendingRow &pending_row) +std::tuple> CrudModel::saveRow(const PendingRow &pending_row, const RowMapping &row_mapping) { auto data = pending_row.data(); - RowMapping& rowmapping = m_rowMapping[pending_row.row()]; + if (data.empty()) + return { true, {} }; - if (!data.empty()) + if (row_mapping.isNew()) { QString buffer; Pgsql::Params params; - if (rowmapping.isNew()) - std::tie(buffer, params) = createInsertQuery(pending_row); - else - { - Pgsql::Params pkey_params = getPKeyParamsForRow(pending_row.row()); - std::tie(buffer, params) = createUpdateQuery(pkey_params, pending_row); - } + std::tie(buffer, params) = createInsertQuery(pending_row); Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); @@ -356,59 +365,120 @@ std::tuple> CrudModel::saveRow(const Pending msgBox.setText(ex.what()); msgBox.exec(); } + return { false, {} }; + } + + return saveUpdate(pending_row, row_mapping); +} + +std::tuple> CrudModel::saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping) +{ + std::set tables; + for (const auto &e : pending_row.data()) + tables.insert(m_mapping->tableOid(e.first)); + + try + { + Pgsql::Connection db_update_conn; + auto dbconfig = m_database->config(); + db_update_conn.connect(dbconfig.connectionString()); + auto tx = Pgsql::Transaction::startTransaction(db_update_conn); + for (Oid table_oid : tables) + { + QString buffer; + Pgsql::Params params; + std::tie(buffer, params) = createUpdateQuery(table_oid, pending_row, row_mapping); + auto result = tx.queryParam(buffer, params); + if (!result || result.tuplesAffected() != 1) + { + QMessageBox::warning(nullptr, tr("Update failed"), tr("The change did not update exactly one row in %1, the row may have changed or is no longer unique.").arg(tableName(table_oid))); + return { false, {} }; + } + } + tx.commit(); + + std::vector values; + values.reserve(m_roData->cols()); + for (int col = 0; col < m_roData->cols(); ++col) + { + auto &&pending = pending_row.data(); + auto found = pending.find(col); + values.push_back(found != pending.end() ? found->second : getSavedData(row_mapping, col)); + } + return { true, values }; + } + catch (const Pgsql::PgResultError &ex) + { + QMessageBox msgBox; + msgBox.setText(ex.what()); + msgBox.exec(); } return { false, {} }; } -Pgsql::Params CrudModel::getPKeyParamsForRow(int row) const +QString CrudModel::tableName(Oid table_oid) const { - Pgsql::Params params; - for (auto attnum : m_primaryKey->key) - { - const int col = attNumToCol(attnum); - Oid t = getType(col); - auto s = getSavedData(col, row); - params.add(s, t); - } - return params; + auto table = m_database->catalog()->classes()->getByKey(table_oid); + return table ? table->fullyQualifiedQuotedObjectName() : QString(); } -QString CrudModel::columnName(int col) const +std::vector CrudModel::resultColumnsOfTable(Oid table_oid) const { - return m_roData->getColName(col); + std::vector cols; + for (int col = 0; col < m_roData->cols(); ++col) + if (m_roData->ftable(col) == table_oid && m_roData->ftableCol(col) > 0) + cols.push_back(col); + return cols; } -std::tuple CrudModel::createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row) +QString CrudModel::attributeName(int col) const { - Pgsql::Params params; - auto data = pending_row.data(); - QString table_name = m_table->fullyQualifiedQuotedObjectName(); - QString buffer; - QTextStream q(&buffer); - q << "UPDATE " << table_name << " AS d\n SET "; - int param = 0; - for (auto& e : data) - { - if (param > 0) - q << ","; - q << quoteIdent(columnName(e.first)) << "=$" << ++param; - params.add(e.second, getType(e.first)); - } + if (col < static_cast(columnList.size()) && columnList[col]) + return columnList[col]->name; + return QString::fromUtf8(m_roData->getColName(col)); +} - q << "\nWHERE "; - int i = 0; - for (auto attnum : m_primaryKey->key) +Pgsql::Params CrudModel::getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const +{ + Pgsql::Params params; + for (int col : m_mapping->primaryKeyColumns(table_oid)) { - int col = attNumToCol(attnum); - if (i > 0) - q << " AND "; - q << quoteIdent(columnName(col)) << "=$" << ++param; - ++i; - } - params.addParams(pkey_params); - q << "\nRETURNING *"; - q.flush(); - return { buffer, params }; + auto s = getSavedData(row_mapping, col); + params.add(s, getType(col)); + } + return params; +} + +std::tuple CrudModel::createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping) +{ + Pgsql::Params params; + QString table_name = tableName(table_oid); + QString buffer; + QTextStream q(&buffer); + q << "UPDATE " << table_name << " AS d\n SET "; + int param = 0; + for (auto& e : pending_row.data()) + { + if (m_mapping->tableOid(e.first) != table_oid) + continue; + if (param > 0) + q << ","; + q << quoteIdent(attributeName(e.first)) << "=$" << ++param; + params.add(e.second, getType(e.first)); + } + + q << "\nWHERE "; + int i = 0; + for (int col : m_mapping->primaryKeyColumns(table_oid)) + { + if (i > 0) + q << " AND "; + q << quoteIdent(attributeName(col)) << "=$" << ++param; + ++i; + } + params.addParams(getPKeyParamsForRow(row_mapping, table_oid)); + q.flush(); + return { buffer, params }; } std::tuple CrudModel::createInsertQuery(const PendingRow &pending_row) @@ -424,12 +494,11 @@ std::tuple CrudModel::createInsertQuery(const PendingRow for (const auto& e : data) { int num = e.first; - auto&& column = columnList[num]; if (first) first = false; else q << ","; - q << quoteIdent(column.name); + q << quoteIdent(attributeName(num)); } q << ") VALUES ($1"; for (size_t p = 2; p <= data.size(); ++p) @@ -442,19 +511,6 @@ std::tuple CrudModel::createInsertQuery(const PendingRow return { buffer, params }; } -std::tuple CrudModel::createDeleteStatement(const PKeyValues &pkey_values) -{ - Pgsql::Params params; - size_t i = 0; - for (auto attnum : m_primaryKey->key) - { - const int col = attNumToCol(attnum); - params.add(pkey_values[i].c_str(), getType(col)); - ++i; - } - - return { createDeleteStatement(), params }; -} QString CrudModel::createDeleteStatement() const { @@ -465,12 +521,11 @@ QString CrudModel::createDeleteStatement() const q << "DELETE FROM " << table_name; q << "\nWHERE "; int i = 0; - for (auto attnum : m_primaryKey->key) + for (int col : m_mapping->primaryKeyColumns(m_table->oid())) { - const int col = attNumToCol(attnum); if (i > 0) q << " AND "; - q << quoteIdent(columnName(col)) << "=$" << ++i; + q << quoteIdent(attributeName(col)) << "=$" << ++i; } q.flush(); @@ -483,7 +538,13 @@ bool CrudModel::savePendingChanges() while (!m_pendingRowList.m_rows.empty()) { auto iter = m_pendingRowList.m_rows.begin(); - auto [ok, modified_row] = saveRow(iter->second); + int pendingKey = iter->first; + auto mapping_iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), + [pendingKey](const RowMapping &rhs) -> bool { return rhs.rowKey == pendingKey; }); + if (mapping_iter == m_rowMapping.end()) + return false; + + auto [ok, modified_row] = saveRow(iter->second, *mapping_iter); if (ok) { int rowKey = iter->first; @@ -516,9 +577,13 @@ bool CrudModel::columnIsReadOnly(int column_index) const if (m_roData == nullptr) return true; - auto&& column = columnList[column_index - PreColumnCount]; - return column.getIdentity() == PgAttribute::Identity::Always - || column.getGenerated() != PgAttribute::Generated::None; + int col = column_index - PreColumnCount; + if (!m_mapping || !m_mapping->isEditable(col)) + return true; + + const auto &column = columnList[col]; + return !column || column->getIdentity() == PgAttribute::Identity::Always + || column->getGenerated() != PgAttribute::Generated::None; } void CrudModel::initializeColumnList() @@ -526,13 +591,20 @@ void CrudModel::initializeColumnList() columnList.clear(); columnList.reserve(m_roData->cols()); auto atts = m_database->catalog()->attributes(); - auto columns = atts->getColumnsForRelation(m_table->oid()); for (int col = 0; col < m_roData->cols(); ++col) { int attnum = m_roData->ftableCol(col); + if (attnum <= 0) + { + columnList.emplace_back(); + continue; + } + auto columns = atts->getColumnsForRelation(m_roData->ftable(col)); auto find_result = std::find_if(columns.begin(), columns.end(), [attnum](const PgAttribute &att) { return att.num == attnum; }); - assert(find_result != columns.end()); - columnList.push_back(*find_result); + if (find_result != columns.end()) + columnList.emplace_back(*find_result); + else + columnList.emplace_back(); } } @@ -548,6 +620,9 @@ void CrudModel::revert() void CrudModel::appendNewRow() { + if (!m_wholeTable) + return; + int row = static_cast(m_rowMapping.size()); beginInsertRows(QModelIndex(), row, row); appendNewRowInternal(); @@ -565,6 +640,8 @@ std::tuple CrudModel::removeRows(const std::set return { true, "" }; if (row_ranges.rbegin()->end() > static_cast(m_rowMapping.size())) return { false, "Range error" }; + if (!m_wholeTable || !m_table || m_mapping->primaryKeyColumns(m_table->oid()).empty()) + return { false, "Rows can only be removed from a table with a primary key" }; // When removing rows there is no direct mapping anymore between the rows in the grid // and the rows in m_roData @@ -589,7 +666,7 @@ std::tuple CrudModel::removeRows(const std::set for (int current_row = range.start(); current_row < range.end(); ++current_row) { auto&& mapping = m_rowMapping[static_cast(current_row)]; - auto params = getPKeyParamsForRow(mapping.rowKey); + auto params = getPKeyParamsForRow(mapping, m_table->oid()); if (!params.empty()) db_update_conn.queryParam(delete_statement, params); } diff --git a/pglab/crud/CrudModel.h b/pglab/crud/CrudModel.h index 5a6beb3..d05b6c0 100644 --- a/pglab/crud/CrudModel.h +++ b/pglab/crud/CrudModel.h @@ -5,8 +5,10 @@ #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 @@ -26,6 +28,8 @@ public: ~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; @@ -50,7 +54,6 @@ public slots: virtual void revert() override; private: - using PKeyValues = std::vector; const int PreColumnCount = 1; @@ -76,6 +79,7 @@ private: res += " NULLS FIRST"; else if (nulls == NullSorting::Last) res += " NULLS LAST"; + return res; } }; @@ -205,10 +209,11 @@ private: std::shared_ptr m_database; std::optional m_table; - std::optional m_primaryKey; + 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; @@ -218,7 +223,7 @@ private: // identity allways or generated }; - std::vector columnList; // list of columnMeta 1 to 1 with columns in m_roData. + std::vector> columnList; // attribute per column in m_roData, empty for expression columns PendingRowList m_pendingRowList; @@ -238,29 +243,30 @@ private: 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; + Pgsql::Params getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const; bool savePendingChanges(); - std::tuple createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row); + std::tuple createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping); std::tuple createInsertQuery(const PendingRow &pending_row); - std::tuple createDeleteStatement(const PKeyValues &pkey_values); QString createDeleteStatement() const; - std::tuple> saveRow(const PendingRow &pending_row); + std::tuple> saveRow(const PendingRow &pending_row, const RowMapping &row_mapping); + std::tuple> saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping); void appendNewRow(); void appendNewRowInternal(); 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; } + /// Name of the table with the given oid, quoted and schema qualified + QString tableName(Oid table_oid) const; + /// Result columns that are user columns (attnum > 0) of the given table, in result order + std::vector resultColumnsOfTable(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; void RemoveRangesOfRowsFromModel(const std::set> &row_ranges); void RemoveRangeOfRowsFromModel(IntegerRange row_range);