#include "CrudModel.h" #include "OpenDatabase.h" #include "catalog/PgDatabaseCatalog.h" #include "catalog/PgAttribute.h" #include "catalog/PgAttributeContainer.h" #include "catalog/PgConstraintContainer.h" #include "SqlFormattingUtils.h" #include "Pgsql_oids.h" #include "CustomDataRole.h" #include #include #include #include "Pgsql_oids.h" #include "Pgsql_PgException.h" #include "Pgsql_Params.h" #include "Pgsql_Transaction.h" #include #include "ScopeGuard.h" CrudModel::CrudModel(QObject *parent) : QAbstractTableModel(parent) , m_dbConn() { 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()); callLoadData = true; auto dbconfig = m_database->config(); m_dbConn.setupConnection(dbconfig); } QVariant CrudModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section == 0) return tr("#"); int column = section - PreColumnCount; QVariant r; if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { QString s(m_roData->getColName(column)); s += "\n"; s += getTypeDisplayString(*m_database->catalog(), getType(column)); r = s; } else r = QString::number(column + 1); } return r; } int CrudModel::rowCount(const QModelIndex &/*parent*/) const { return (int)m_rowMapping.size(); } int CrudModel::columnCount(const QModelIndex &/*parent*/) const { int col_count = m_roData ? m_roData->cols() : 0; return PreColumnCount + col_count; } Oid CrudModel::getType(int column) const { return m_roData ? m_roData->type(column) : InvalidOid; } CrudModel::Value CrudModel::getLatestData(int columnIndex, int rowIndex) const { if (m_roData) { auto row_mapping = m_rowMapping[rowIndex]; std::optional val; if (row_mapping.pending) val = m_pendingRowList.getValue(columnIndex, row_mapping.rowKey); if (!val.has_value()) val = getSavedData(row_mapping, columnIndex); return val.value_or(Value()); } return {}; } CrudModel::Value CrudModel::getSavedData(int columnIndex, int rowIndex) const { if (m_roData) { auto row_mapping = m_rowMapping[rowIndex]; return getSavedData(row_mapping, columnIndex); } return {}; } CrudModel::Value CrudModel::getSavedData(const RowMapping &row_mapping, int columnIndex) const { std::optional val; if (!val.has_value() && row_mapping.isModified()) val = row_mapping.modifiedValue(columnIndex); if (!val.has_value() && row_mapping.rowKey < m_roData->rows()) if (!m_roData->null(columnIndex, row_mapping.rowKey)) val = std::string(m_roData->val(columnIndex, row_mapping.rowKey)); return val.value_or(Value()); } QVariant StringToVariant(const std::string &str, Oid oid) { Pgsql::Value v(str.c_str(), oid); switch (oid) { default: return QString::fromStdString(str); case Pgsql::timestamp_oid: case Pgsql::timestamptz_oid: return v.operator QDateTime(); case Pgsql::date_oid: return v.operator QDate(); case Pgsql::time_oid: case Pgsql::timetz_oid: return v.operator QTime(); case Pgsql::int2_oid: return v.operator int16_t(); case Pgsql::int4_oid: return v.operator int32_t(); case Pgsql::oid_oid: case Pgsql::int8_oid: return v.operator int64_t(); case Pgsql::bool_oid: return v.operator bool(); case Pgsql::float4_oid: return v.operator float(); case Pgsql::float8_oid: return v.operator double(); } } QVariant CrudModel::data(const QModelIndex &index, int role) const { if (index.column() < PreColumnCount) { if (role == Qt::DisplayRole || role == CustomSortRole) return index.row() + 1; else if (role == CustomDataTypeRole) return Pgsql::int4_oid; return {}; } Oid typ = getType(index.column() - PreColumnCount); if (role == Qt::EditRole || role == Qt::DisplayRole) { auto value = getLatestData(index); if (value) { if (role == Qt::EditRole) return QString::fromUtf8(value->c_str()); else { if (typ == Pgsql::bool_oid) return *value == "t"; else { QString s = QString::fromUtf8(value->c_str()); s.truncate(256); return s; } } } } else if (role == CustomDataTypeRole) return typ; else if (role == CustomSortRole) { auto value = getLatestData(index); if (value) return StringToVariant(value->c_str(), typ); } return {}; } void CrudModel::loadData() { QString table_name = m_table->fullyQualifiedQuotedObjectName(); std::string q = "SELECT * FROM "; q += table_name.toStdString(); m_dbConn.send(q, [this] (Expected> res, qint64) { if (res.valid()) { auto dbres = res.get(); if (dbres && *dbres) QMetaObject::invokeMethod(this, "loadIntoModel", Qt::QueuedConnection, Q_ARG(std::shared_ptr, dbres)); } }); } void CrudModel::loadIntoModel(std::shared_ptr data) { beginResetModel(); m_pendingRowList.clear(); m_roData = data; initializeColumnList(); lastRowKey = data->rows() - 1; initRowMapping(); appendNewRowInternal(); endResetModel(); } void CrudModel::initRowMapping() { size_t cnt = m_roData->rows(); m_rowMapping.clear(); m_rowMapping.reserve(cnt + 1); for (int i = 0; i < cnt; ++i) m_rowMapping.emplace_back(i); } void CrudModel::connectionStateChanged() { switch (m_dbConn.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 &index) const { Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (index.column() < PreColumnCount) return flags; if (m_primaryKey && !columnIsReadOnly(index.column())) flags |= Qt::ItemIsEditable; return flags; } bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.column() < PreColumnCount) return false; if (role == Qt::EditRole) { int grid_row = index.row(); int col = index.column() - PreColumnCount; 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; } std::tuple> CrudModel::saveRow(const PendingRow &pending_row) { auto data = pending_row.data(); RowMapping& rowmapping = m_rowMapping[pending_row.row()]; if (!data.empty()) { 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); } Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); db_update_conn.connect(dbconfig.connectionString()); try { 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())); } 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 { 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; } QString CrudModel::columnName(int col) const { return m_roData->getColName(col); } std::tuple CrudModel::createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row) { 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)); } 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; ++i; } params.addParams(pkey_params); 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(); QString buffer; QTextStream q(&buffer); q << "INSERT INTO " << table_name << "("; bool first = true; for (const auto& e : data) { int num = e.first; auto&& column = columnList[num]; if (first) first = false; else q << ","; q << quoteIdent(column.name); } q << ") VALUES ($1"; for (size_t p = 2; p <= data.size(); ++p) q << ",$" << p; q << ") RETURNING *"; for (auto& e : data) 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; } bool CrudModel::savePendingChanges() { while (!m_pendingRowList.m_rows.empty()) { auto iter = m_pendingRowList.m_rows.begin(); auto [ok, modified_row] = saveRow(iter->second); if (ok) { int rowKey = iter->first; m_pendingRowList.m_rows.erase(iter); auto iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), [rowKey](const RowMapping &rhs) -> bool { return rhs.rowKey == rowKey; }); if (iter != m_rowMapping.end()) { iter->setModifiedRowData(modified_row); iter->pending = false; if (IsLastRow(iter)) appendNewRow(); } } else return false; } return true; } bool CrudModel::IsLastRow(RowMappingVector::iterator mapping_iter) const { return mapping_iter == --m_rowMapping.end(); } 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; } void CrudModel::initializeColumnList() { columnList.clear(); columnList.reserve(m_roData->cols()); auto columns = m_database->catalog()->attributes()->getColumnsForRelation(m_table->oid()); for (int col = 0; col < m_roData->cols(); ++col) { int attnum = m_roData->ftableCol(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); } } bool CrudModel::submit() { return savePendingChanges(); } void CrudModel::revert() { } void CrudModel::appendNewRow() { int row = static_cast(m_rowMapping.size()); beginInsertRows(QModelIndex(), row, row); appendNewRowInternal(); endInsertRows(); } void CrudModel::appendNewRowInternal() { m_rowMapping.emplace_back(allocNewRowKey(), std::vector(m_roData->cols())); } std::tuple CrudModel::removeRows(const std::set> &row_ranges) { if (row_ranges.empty()) return { true, "" }; if (row_ranges.rbegin()->end() > static_cast(m_rowMapping.size())) return { false, "Range error" }; // 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 try { Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); db_update_conn.connect(dbconfig.connectionString()); // First delete rows in table QString delete_statement = createDeleteStatement(); { auto tx = Pgsql::Transaction::startTransaction(db_update_conn); for (auto range : row_ranges) { 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); if (!params.empty()) db_update_conn.queryParam(delete_statement, params); } } tx.commit(); // If something goes wrong after this commit we should reload contents of model } // Then from model RemoveRangesOfRowsFromModel(row_ranges); return { true, "" }; } catch (const Pgsql::PgResultError &error) { return { false, QString::fromUtf8(error.details().messageDetail.c_str()) }; } } bool CrudModel::removeRows(int row, int count, const QModelIndex &) { if (m_rowMapping.empty()) return false; IntegerRange range(row, count); auto [res, message] = removeRows({ range }); return res; } void CrudModel::RemoveRangesOfRowsFromModel(const std::set> &row_ranges) { int rows_deleted = 0; for (auto range : row_ranges) { range.setStart(range.start() - rows_deleted); RemoveRangeOfRowsFromModel(range); rows_deleted += range.length(); } } void CrudModel::RemoveRangeOfRowsFromModel(IntegerRange range) { beginRemoveRows(QModelIndex(), range.start(), range.end() - 1); SCOPE_EXIT { endRemoveRows(); }; auto first = m_rowMapping.begin() + range.start(); m_rowMapping.erase(first, first + range.length()); }