Improve CrudModel code quality

This commit is contained in:
eelke 2026-10-18 17:55:26 +02:00
parent 82cc9f14bc
commit 550049ceff
6 changed files with 870 additions and 205 deletions

View file

@ -6,7 +6,6 @@
#include "catalog/PgConstraintContainer.h"
#include "catalog/PgClassContainer.h"
#include "SqlFormattingUtils.h"
#include "Pgsql_oids.h"
#include "CustomDataRole.h"
#include <QtConcurrent>
#include <QFuture>
@ -98,45 +97,46 @@ Oid CrudModel::getType(int column) const
CrudModel::Value CrudModel::getLatestData(int columnIndex, int rowIndex) const
{
if (m_roData)
if (!m_roData)
return {};
const RowMapping &row_mapping = m_rowMapping[rowIndex];
if (row_mapping.pending)
{
auto row_mapping = m_rowMapping[rowIndex];
std::optional<Value> 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());
// an edited cell can be NULL, that is different from not having been edited
if (auto edited = m_pendingRowList.getValue(columnIndex, row_mapping.rowKey))
return *edited;
}
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 {};
return getSavedData(row_mapping, columnIndex);
}
CrudModel::Value CrudModel::getSavedData(const RowMapping &row_mapping, int columnIndex) const
{
std::optional<Value> val;
if (!val.has_value() && row_mapping.isModified())
val = row_mapping.modifiedValue(columnIndex);
// rows that have been saved (or are new) have their own copy of the data
if (row_mapping.isModified())
return 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));
if (row_mapping.rowKey < m_roData->rows() && !m_roData->null(columnIndex, row_mapping.rowKey))
return std::string(m_roData->val(columnIndex, row_mapping.rowKey));
return val.value_or(Value());
return {};
}
CrudModel::RowMapping* CrudModel::findRowMapping(int rowKey)
{
auto iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(),
[rowKey](const RowMapping &rhs) { return rhs.rowKey == rowKey; });
return iter != m_rowMapping.end() ? &*iter : nullptr;
}
void CrudModel::updatePendingState(bool was_dirty)
{
if (was_dirty != hasPendingChanges())
emit pendingChangesChanged(hasPendingChanges());
}
namespace {
QVariant StringToVariant(const std::string &str, Oid oid)
{
Pgsql::Value v(str.c_str(), oid);
@ -174,9 +174,10 @@ QVariant StringToVariant(const std::string &str, Oid oid)
case Pgsql::float8_oid:
return v.operator double();
}
}
} // namespace
QVariant CrudModel::data(const QModelIndex &index, int role) const
{
if (index.column() < PreColumnCount)
@ -254,8 +255,7 @@ void CrudModel::loadIntoModel(std::shared_ptr<Pgsql::Result> data)
if (m_wholeTable)
appendNewRowInternal();
endResetModel();
if (was_dirty)
emit pendingChangesChanged(false);
updatePendingState(was_dirty);
}
void CrudModel::initRowMapping()
@ -269,26 +269,11 @@ void CrudModel::initRowMapping()
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;
}
if (m_dbConn.state() == ASyncDBConnection::State::Connected && callLoadData)
{
callLoadData = false;
loadData();
}
}
Qt::ItemFlags CrudModel::flags(const QModelIndex &index) const
@ -326,8 +311,7 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol
}
bool was_dirty = hasPendingChanges();
m_pendingRowList.setValue(col, row_mapping.rowKey, val);
if (!was_dirty)
emit pendingChangesChanged(true);
updatePendingState(was_dirty);
emit dataChanged(index, index, QVector<int>() << role);
return true;
@ -337,8 +321,7 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol
std::tuple<bool, std::vector<CrudModel::Value>> CrudModel::saveRow(const PendingRow &pending_row, const RowMapping &row_mapping)
{
auto data = pending_row.data();
if (data.empty())
if (pending_row.data().empty())
return { true, {} };
if (row_mapping.isNew())
@ -348,31 +331,16 @@ std::tuple<bool, std::vector<CrudModel::Value>> CrudModel::saveRow(const Pending
std::tie(buffer, params) = createInsertQuery(pending_row);
Pgsql::Connection db_update_conn;
auto dbconfig = m_database->config();
db_update_conn.connect(dbconfig.connectionString());
connectForUpdate(db_update_conn);
try
{
auto result = db_update_conn.queryParam(buffer, params);
if (result && result.rows() == 1)
{
std::vector<Value> 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 };
}
return { true, rowValues(*result.begin()) };
}
catch (const Pgsql::PgResultError &ex)
{
QMessageBox msgBox;
msgBox.setText(ex.what());
msgBox.exec();
reportError(ex.what());
}
return { false, {} };
}
@ -389,8 +357,7 @@ std::tuple<bool, std::vector<CrudModel::Value>> CrudModel::saveUpdate(const Pend
try
{
Pgsql::Connection db_update_conn;
auto dbconfig = m_database->config();
db_update_conn.connect(dbconfig.connectionString());
connectForUpdate(db_update_conn);
auto tx = Pgsql::Transaction::startTransaction(db_update_conn);
for (Oid table_oid : tables)
{
@ -418,32 +385,47 @@ std::tuple<bool, std::vector<CrudModel::Value>> CrudModel::saveUpdate(const Pend
}
catch (const Pgsql::PgResultError &ex)
{
QMessageBox msgBox;
msgBox.setText(ex.what());
msgBox.exec();
reportError(ex.what());
}
return { false, {} };
}
void CrudModel::connectForUpdate(Pgsql::Connection &connection) const
{
auto dbconfig = m_database->config();
connection.connect(dbconfig.connectionString());
}
void CrudModel::reportError(const QString &message) const
{
QMessageBox msgBox;
msgBox.setText(message);
msgBox.exec();
}
std::vector<CrudModel::Value> CrudModel::rowValues(const Pgsql::Row &row)
{
std::vector<Value> values;
for (auto v : row)
{
if (v.null())
values.push_back(Value());
else
values.push_back(std::string(v.c_str()));
}
return values;
}
QString CrudModel::tableName(Oid table_oid) const
{
auto table = m_database->catalog()->classes()->getByKey(table_oid);
return table ? table->fullyQualifiedQuotedObjectName() : QString();
}
std::vector<int> CrudModel::resultColumnsOfTable(Oid table_oid) const
{
std::vector<int> 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;
}
QString CrudModel::attributeName(int col) const
{
if (col < static_cast<int>(columnList.size()) && columnList[col])
return columnList[col]->name;
if (col < static_cast<int>(m_columnList.size()) && m_columnList[col])
return m_columnList[col]->name;
return QString::fromUtf8(m_roData->getColName(col));
}
@ -493,7 +475,7 @@ std::tuple<QString, Pgsql::Params> CrudModel::createUpdateQuery(Oid table_oid, c
std::tuple<QString, Pgsql::Params> CrudModel::createInsertQuery(const PendingRow &pending_row)
{
Pgsql::Params params;
auto data = pending_row.data();
const auto &data = pending_row.data();
QString table_name = m_table->fullyQualifiedQuotedObjectName();
QString buffer;
QTextStream q(&buffer);
@ -523,7 +505,6 @@ std::tuple<QString, Pgsql::Params> CrudModel::createInsertQuery(const PendingRow
QString CrudModel::createDeleteStatement() const
{
Pgsql::Params params;
QString table_name = m_table->fullyQualifiedQuotedObjectName();
QString buffer;
QTextStream q(&buffer);
@ -547,40 +528,25 @@ bool CrudModel::savePendingChanges()
while (!m_pendingRowList.m_rows.empty())
{
auto iter = m_pendingRowList.m_rows.begin();
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())
RowMapping *mapping = findRowMapping(iter->first);
if (!mapping)
return false;
auto [ok, modified_row] = saveRow(iter->second, *mapping_iter);
if (ok)
{
int rowKey = iter->first;
m_pendingRowList.m_rows.erase(iter);
auto [ok, modified_row] = saveRow(iter->second, *mapping);
if (!ok)
return false;
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;
m_pendingRowList.m_rows.erase(iter);
mapping->setModifiedRowData(modified_row);
mapping->pending = false;
// appending invalidates mapping, so keep this last
if (mapping == &m_rowMapping.back())
appendNewRow();
}
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)
@ -590,30 +556,30 @@ bool CrudModel::columnIsReadOnly(int column_index) const
if (!m_mapping || !m_mapping->isEditable(col))
return true;
const auto &column = columnList[col];
const auto &column = m_columnList[col];
return !column || column->getIdentity() == PgAttribute::Identity::Always
|| column->getGenerated() != PgAttribute::Generated::None;
}
void CrudModel::initializeColumnList()
{
columnList.clear();
columnList.reserve(m_roData->cols());
m_columnList.clear();
m_columnList.reserve(m_roData->cols());
auto atts = m_database->catalog()->attributes();
for (int col = 0; col < m_roData->cols(); ++col)
{
int attnum = m_roData->ftableCol(col);
if (attnum <= 0)
{
columnList.emplace_back();
m_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; });
if (find_result != columns.end())
columnList.emplace_back(*find_result);
m_columnList.emplace_back(*find_result);
else
columnList.emplace_back();
m_columnList.emplace_back();
}
}
@ -621,8 +587,7 @@ bool CrudModel::saveChanges()
{
bool was_dirty = hasPendingChanges();
bool ok = savePendingChanges();
if (was_dirty != hasPendingChanges())
emit pendingChangesChanged(hasPendingChanges());
updatePendingState(was_dirty);
return ok;
}
@ -664,49 +629,42 @@ std::tuple<bool, QString> CrudModel::removeRows(const std::set<IntegerRange<int>
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
// 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
// When removing rows there is no direct mapping anymore between the rows in the grid
// and the rows in m_roData, that is why the grid rows are mapped through m_rowMapping.
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<size_t>(current_row)];
auto params = getPKeyParamsForRow(mapping, m_table->oid());
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
deleteRowsFromDatabase(row_ranges);
// If something goes wrong after the commit we should reload contents of model
bool was_dirty = hasPendingChanges();
RemoveRangesOfRowsFromModel(row_ranges);
if (was_dirty != hasPendingChanges())
emit pendingChangesChanged(hasPendingChanges());
return { true, "" };
removeRangesFromModel(row_ranges);
updatePendingState(was_dirty);
return { true, "" };
}
catch (const Pgsql::PgResultError &error)
{
return { false, QString::fromUtf8(error.details().messageDetail.c_str()) };
}
return { false, QString::fromUtf8(error.details().messageDetail.c_str()) };
}
}
void CrudModel::deleteRowsFromDatabase(const std::set<IntegerRange<int>> &row_ranges)
{
Pgsql::Connection db_update_conn;
connectForUpdate(db_update_conn);
QString delete_statement = createDeleteStatement();
auto tx = Pgsql::Transaction::startTransaction(db_update_conn);
for (const auto &range : row_ranges)
{
for (int current_row = range.start(); current_row < range.end(); ++current_row)
{
const auto &mapping = m_rowMapping[static_cast<size_t>(current_row)];
auto params = getPKeyParamsForRow(mapping, m_table->oid());
if (!params.empty())
db_update_conn.queryParam(delete_statement, params);
}
}
tx.commit();
}
bool CrudModel::removeRows(int row, int count, const QModelIndex &)
@ -719,18 +677,18 @@ bool CrudModel::removeRows(int row, int count, const QModelIndex &)
return res;
}
void CrudModel::RemoveRangesOfRowsFromModel(const std::set<IntegerRange<int>> &row_ranges)
void CrudModel::removeRangesFromModel(const std::set<IntegerRange<int>> &row_ranges)
{
int rows_deleted = 0;
for (auto range : row_ranges)
{
range.setStart(range.start() - rows_deleted);
RemoveRangeOfRowsFromModel(range);
removeRangeFromModel(range);
rows_deleted += range.length();
}
}
void CrudModel::RemoveRangeOfRowsFromModel(IntegerRange<int> range)
void CrudModel::removeRangeFromModel(IntegerRange<int> range)
{
beginRemoveRows(QModelIndex(), range.start(), range.end() - 1);
SCOPE_EXIT { endRemoveRows(); };