#include "CrudModel.h" #include "ASyncWindow.h" #include "OpenDatabase.h" #include "PgDatabaseCatalog.h" #include "PgAttribute.h" #include "PgAttributeContainer.h" #include "PgConstraintContainer.h" #include "GlobalIoService.h" #include "SqlFormattingUtils.h" #include "WorkManager.h" #include "Pgsql_oids.h" #include #include #include #include "Pgsql_oids.h" #include "Pgsql_Params.h" #include #include "ScopeGuard.h" CrudModel::CrudModel(ASyncWindow *async_window) : m_asyncWindow(async_window) , m_dbConn(*getGlobalAsioIoService()) { qDebug("CrudModel created"); connect(&m_dbConn, &ASyncDBConnection::onStateChanged, this, &CrudModel::connectionStateChanged); } CrudModel::~CrudModel() { m_dbConn.closeConnection(); } /* * Strategy * when ordered by primary key, offset and limit work very quickly so we can get away with not loading * everything. */ void CrudModel::setConfig(std::shared_ptr db, const PgClass &table) { m_database = db; m_table = table; m_primaryKey = db->catalog()->constraints()->getPrimaryForRelation(table.oid()); //cat->attributes()->getColumnsForRelation() callLoadData = true; auto dbconfig = m_database->config(); m_dbConn.setupConnection(dbconfig); } QVariant CrudModel::headerData(int section, Qt::Orientation orientation, int role) const { QVariant r; if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { QString s(m_roData->getColName(section)); s += "\n"; s += getTypeDisplayString(*m_database->catalog(), getType(section)); r = s; } else { r = QString::number(section + 1); } } return r; } // Basic functionality: int CrudModel::rowCount(const QModelIndex &/*parent*/) const { // int row_count = m_roData ? m_roData->rows() : 0; // todo there will be rownumbers that are not in m_roData // ++row_count; // one empty new row at the end return m_rowCount; } int CrudModel::columnCount(const QModelIndex &/*parent*/) const { int col_count = m_roData ? m_roData->cols() : 0; return col_count; } Oid CrudModel::getType(int column) const { return m_roData ? m_roData->type(column) : InvalidOid; } CrudModel::Value CrudModel::getData(const QModelIndex &index) const { Value value; if (m_roData) { int grid_row = index.row(); int col = index.column(); auto row_mapping = m_rowMapping[grid_row]; const int last_row = rowCount() - 1; //Oid o = m_roData->type(col); // First see if we have buffered editted values that still need saving std::optional val; if (row_mapping.pending) { val = m_pendingRowList.getValue(col, row_mapping.rowKey); } if (row_mapping.modified && !val && grid_row < last_row) { // last_row should never be in modified list, when it is put in modified list a new last row should be created // No pending save have a look if we have modified saved data in the modified list auto find_res = m_modifiedRowList.find(row_mapping.rowKey); if (find_res != m_modifiedRowList.end()) { val = find_res->second.data()[col]; } } //Value value; // If we did not have pending or modified data if (!val && row_mapping.rowKey < m_roData->rows()) { // Then we are going to read the original data. if (!m_roData->null(col, row_mapping.rowKey)) { value = std::string(m_roData->val(col, row_mapping.rowKey)); } } else { if (val) { value = *val; } } } return value; } QVariant CrudModel::data(const QModelIndex &index, int role) const { QVariant v; if (role == Qt::EditRole) { auto value = getData(index); if (value) { QString s = QString::fromUtf8(value->c_str()); v = s; } } else if (role == Qt::DisplayRole) { auto value = getData(index); if (value) { Oid o = m_roData->type(index.column()); if (o == Pgsql::bool_oid) { v = *value == "t"; //s = (s == "t") ? "TRUE" : "FALSE"; } else { QString s = QString::fromUtf8(value->c_str()); if (s.length() > 256) { s.truncate(256); } v = s; } } } else if (role == Qt::UserRole) { v = getType(index.column()); } return v; } void CrudModel::loadData() { QString table_name = m_table->fullyQualifiedQuotedObjectName(); // genFQTableName(*m_database->catalog(), *m_table); std::string q = "SELECT * FROM "; q += std::string(table_name.toUtf8().data()); m_dbConn.send(q, [this] (Expected> res, qint64) { if (res.valid()) { auto dbres = res.get(); if (dbres && *dbres) { m_asyncWindow->QueueTask([this, dbres]() { loadIntoModel(dbres); }); } } else { // emit onQueryError(); } }); } void CrudModel::loadIntoModel(std::shared_ptr data) { beginResetModel(); m_pendingRowList.clear(); m_modifiedRowList.clear(); m_roData = data; lastRowKey = data->rows(); m_rowCount = data->rows(); initRowMapping(); appendNewRow(); endResetModel(); } void CrudModel::initRowMapping() { m_rowMapping.resize(m_rowCount); for (int i = 0; i < m_rowCount; ++i) m_rowMapping[i] = { i }; } void CrudModel::connectionStateChanged(ASyncDBConnection::State state) { switch (state) { case ASyncDBConnection::State::NotConnected: break; case ASyncDBConnection::State::Connecting: break; case ASyncDBConnection::State::Connected: if (callLoadData) { callLoadData = false; loadData(); } break; case ASyncDBConnection::State::QuerySend: break; case ASyncDBConnection::State::CancelSend: break; case ASyncDBConnection::State::Terminating: break; } } Qt::ItemFlags CrudModel::flags(const QModelIndex &) const { Qt::ItemFlags flags = Qt::ItemIsSelectable + Qt::ItemIsEnabled; if (m_primaryKey) { flags |= Qt::ItemIsEditable; } return flags; } bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role == Qt::EditRole) { int grid_row = index.row(); int col = index.column(); auto& row_mapping = m_rowMapping[grid_row]; row_mapping.pending = true; Value val; std::string s = value.toString().toUtf8().data(); if (!s.empty()) { if (s == "''") s.clear(); val = s; } m_pendingRowList.setValue(col, row_mapping.rowKey, val); emit dataChanged(index, index, QVector() << role); return true; } return false; } const CrudModel::ModifiedRow* CrudModel::getModifiedRow(int row) const { auto iter = m_modifiedRowList.find(row); if (iter == m_modifiedRowList.end()) return nullptr; else return &iter->second; } CrudModel::PKeyValues CrudModel::getPKeyForRow(int row) const { PKeyValues values; values.reserve(m_primaryKey->key.size()); auto mod_row = getModifiedRow(row); if (mod_row){ for (auto attnum : m_primaryKey->key) { const int col = attNumToCol(attnum); values.push_back(*(mod_row->data()[col])); } } else if (row < m_roData->rows()){ for (auto attnum : m_primaryKey->key) { int col = attNumToCol(attnum); values.push_back(m_roData->get(col, row).c_str()); } } return values; } Pgsql::Params CrudModel::getPKeyParamsForRow(int row) const { Pgsql::Params params; auto mod_row = getModifiedRow(row); for (auto attnum : m_primaryKey->key) { const int col = attNumToCol(attnum); Oid t = getType(col); std::string s; if (mod_row){ s = *(mod_row->data()[col]); } else if (row < m_roData->rows()){ s = m_roData->get(col, row).c_str(); } params.add(s, t); } return params; } QString CrudModel::columnName(int col) const { return m_roData->getColName(col); } std::tuple CrudModel::createUpdateQuery(const PKeyValues &pkey_values, const PendingRow &pending_row) { Pgsql::Params params; auto data = pending_row.data(); QString table_name = m_table->fullyQualifiedQuotedObjectName(); //genFQTableName(*m_database->catalog(), *m_table); 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; // Add value to paramlist params.add(e.second, getType(e.first)); } q << "\nWHERE "; int i = 0; for (auto attnum : m_primaryKey->key) { int col = attNumToCol(attnum); if (i > 0) q << " AND "; q << quoteIdent(columnName(col)) << "=$" << ++param; params.add(pkey_values[i].c_str(), getType(col)); ++i; } q << "\nRETURNING *"; q.flush(); return { buffer, params }; } std::tuple CrudModel::createInsertQuery(const PendingRow &pending_row) { Pgsql::Params params; auto data = pending_row.data(); QString table_name = m_table->fullyQualifiedQuotedObjectName(); // genFQTableName(*m_database->catalog(), *m_table); QString buffer; QTextStream q(&buffer); q << "INSERT INTO " << table_name << "("; auto columns = m_database->catalog()->attributes()->getColumnsForRelation(m_table->oid()); bool first = true; for (auto e : data) { int num = e.first + 1; auto find_res = std::find_if(columns.begin(), columns.end(), [num] (const auto &elem) -> bool { return num == elem.num; }); if (find_res != columns.end()) { if (first) first = false; else q << ","; q << find_res->name; } } q << ") VALUES ($1"; for (size_t p = 2; p <= data.size(); ++p) q << ",$" << p; q << ") RETURNING *"; for (auto e : data) { // Add value to paramlist params.add(e.second, getType(e.first)); } q.flush(); 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 { Pgsql::Params params; QString table_name = m_table->fullyQualifiedQuotedObjectName(); QString buffer; QTextStream q(&buffer); q << "DELETE FROM " << table_name; q << "\nWHERE "; int i = 0; for (auto attnum : m_primaryKey->key) { const int col = attNumToCol(attnum); if (i > 0) q << " AND "; q << quoteIdent(columnName(col)) << "=$" << ++i; } q.flush(); return buffer; } std::tuple CrudModel::updateRow(const PendingRow &pending_row) { auto data = pending_row.data(); if (!data.empty()) { auto pkey_values = getPKeyForRow(pending_row.row()); QString buffer; Pgsql::Params params; if (pkey_values.empty()){ std::tie(buffer, params) = createInsertQuery(pending_row); } else { std::tie(buffer, params) = createUpdateQuery(pkey_values, pending_row); } int row_number = pending_row.row(); Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); bool res = db_update_conn.connect(dbconfig.getKeywords(), dbconfig.getValues(), false); if (res) { auto result = db_update_conn.queryParam(buffer, params); if (result && result.rows() == 1) { std::vector values; auto row = *result.begin(); for (auto v : row) { if (v.null()) values.push_back(Value()); else values.push_back(std::string(v.c_str())); } ModifiedRow modified_row(row_number, values); return { true, modified_row }; } } } return { false, {} }; } bool CrudModel::savePendingChanges() { while (!m_pendingRowList.m_rows.empty()) { auto iter = m_pendingRowList.m_rows.begin(); auto [ok, modified_row] = updateRow(iter->second); if (ok) { int rowKey = iter->first; m_modifiedRowList.insert_or_assign(rowKey, modified_row); m_pendingRowList.m_rows.erase(iter); auto iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), [rowKey](const RowMapping &rhs) -> auto { return rhs.rowKey == rowKey; }); if (iter != m_rowMapping.end()) { iter->modified = true; iter->pending = false; int row = iter - m_rowMapping.begin(); if (row == m_rowCount - 1) appendNewRow(); } } else { return false; } } return true; } bool CrudModel::submit() { return savePendingChanges(); } void CrudModel::revert() { } void CrudModel::appendNewRow() { int row = m_rowCount; beginInsertRows(QModelIndex(), row, row); m_rowMapping.emplace_back(allocNewRowKey()); ++m_rowCount; endInsertRows(); } void CrudModel::removeRows() { // determine selection // remove selected rows } bool CrudModel::removeRows(int row, int count, const QModelIndex &parent) { if (m_rowMapping.empty()) return false; // When removing rows there is no direct mapping anymore between the rows in the grid // and the rows in m_roData // Therefor we need an indirection to keep track of which rows are visible and which // grid row maps to what data row. Maybe we can also keep track where the current data // of the row is located original data, pending data or modified data // 1. Get PKEY and remove that row from table Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); bool res = db_update_conn.connect(dbconfig.getKeywords(), dbconfig.getValues(), false); if (!res) { return false; } // First delete rows in table QString delete_statement = createDeleteStatement(); db_update_conn.query("BEGIN;"); for (int current_row = row; current_row < row + count; ++current_row) { auto&& mapping = m_rowMapping[static_cast(current_row)]; auto params = getPKeyParamsForRow(mapping.rowKey); if (!params.empty()) { // Execute DELETE auto result = db_update_conn.queryParam(delete_statement, params); } } db_update_conn.query("COMMIT;"); // Then from model { beginRemoveRows(parent, row, row); SCOPE_EXIT { endRemoveRows(); }; for (int current_row = row; current_row < row + count; ++current_row) { auto&& mapping = m_rowMapping[static_cast(current_row)]; // if the row is in modified it can be removed from modified if (mapping.modified) { m_modifiedRowList.erase(mapping.rowKey); } /// \todo can it be pending? should be removed if it is. } // remove the rows from m_rowMapping auto first = m_rowMapping.begin() + row; m_rowMapping.erase(first, first + count); } return true; }