pgLab/pglab/CrudModel.cpp
eelke 15bee33076 Made step to remove ASyncWindow in favour of usage of Future and FutureWatcher.
This should allow concurrency in the plugins to be independent from their container.

Contains also some work on the system for registering plugins.
2018-12-30 15:46:15 +01:00

587 lines
15 KiB
C++

#include "CrudModel.h"
#include "ASyncWindow.h"
#include "OpenDatabase.h"
#include "catalog/PgDatabaseCatalog.h"
#include "catalog/PgAttribute.h"
#include "catalog/PgAttributeContainer.h"
#include "catalog/PgConstraintContainer.h"
#include "GlobalIoService.h"
#include "SqlFormattingUtils.h"
#include "WorkManager.h"
#include "Pgsql_oids.h"
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include "Pgsql_oids.h"
#include "Pgsql_PgException.h"
#include "Pgsql_Params.h"
#include "Pgsql_Transaction.h"
#include <string>
#include "ScopeGuard.h"
CrudModel::CrudModel(QObject *parent)
: QAbstractTableModel(parent)
, 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<OpenDatabase> 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<Value> 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<std::shared_ptr<Pgsql::Result>> res, qint64) {
if (res.valid()) {
auto dbres = res.get();
if (dbres && *dbres) {
/// \todo Use ControllableTask instead with QFUtureWatcher
// m_asyncWindow->QueueTask([this, dbres]() { loadIntoModel(dbres); });
}
}
else {
// emit onQueryError();
}
});
}
void CrudModel::loadIntoModel(std::shared_ptr<Pgsql::Result> 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] = RowMapping{ 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<int>() << 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<QString, Pgsql::Params> 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<QString, Pgsql::Params> 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<QString, Pgsql::Params> 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<bool, CrudModel::ModifiedRow> 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();
db_update_conn.connect(dbconfig.getKeywords(), dbconfig.getValues(), false);
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()));
}
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();
}
std::tuple<bool, QString> CrudModel::removeRows(const std::set<IntegerRange<int>> &row_ranges)
{
if (row_ranges.empty()) return { true, "" };
if (row_ranges.rbegin()->end() > static_cast<int>(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.getKeywords(), dbconfig.getValues(), false);
// First delete rows in table
QString delete_statement = createDeleteStatement();
{
db_update_conn.query("BEGIN;");
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.rowKey);
if (!params.empty()) {
// Execute DELETE
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
{
int rows_deleted = 0;
for (auto range : row_ranges) {
range.setStart(range.start() - rows_deleted);
beginRemoveRows(QModelIndex(), range.start(), range.end() - 1);
SCOPE_EXIT { endRemoveRows(); };
for (int current_row = range.start(); current_row < range.end(); ++current_row) {
auto&& mapping = m_rowMapping[static_cast<size_t>(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() + range.start();
m_rowMapping.erase(first, first + range.length());
rows_deleted += range.length();
}
}
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<int> range(row, count);
auto [res, message] = removeRows({ range });
return res;
}