Improved support from removing rows in crud tabs.

It can handle now complex selections and reports back errors encountered
when removing the rows fails.
This commit is contained in:
eelke 2018-12-15 11:24:58 +01:00
parent 950fea873c
commit 62c6ad5bfb
10 changed files with 365 additions and 116 deletions

136
core/IntegerRange.h Normal file
View file

@ -0,0 +1,136 @@
#ifndef INTEGERRANGE_H
#define INTEGERRANGE_H
#include <algorithm>
template <typename Value>
class IntegerRange {
public:
using Val = Value;
IntegerRange() = default;
IntegerRange(Value start, Value length)
: m_start(start)
, m_length(length)
{}
bool canCombine(const IntegerRange &rhs) const
{
if (m_start == rhs.m_start) {
return true;
}
if (m_start < rhs.m_start) {
return m_start + m_length >= rhs.m_start;
}
else {
return rhs.canCombine(*this);
}
}
void setLength(Value length)
{
m_length = length;
}
Value length() const { return m_length; }
void setStart(Value start)
{
m_start = start;
}
Value start() const { return m_start; }
void setEnd(Value end)
{
m_length = end - m_start;
}
Value end() const { return m_start + m_length; }
bool operator==(const IntegerRange &rhs) const
{
return m_start == rhs.m_start && m_length == rhs.m_length;
}
bool operator!=(const IntegerRange &rhs) const
{
return !operator==(rhs);
}
bool operator<(const IntegerRange &rhs) const
{
return m_start < rhs.m_start;
}
bool operator<=(const IntegerRange &rhs) const
{
return m_start < rhs.m_start || (m_start == rhs.m_start && m_length < rhs.m_length);
}
bool operator>(const IntegerRange &rhs) const
{
return m_start > rhs.m_start;
}
bool operator>=(const IntegerRange &rhs) const
{
return m_start > rhs.m_start || (m_start == rhs.m_start && m_length > rhs.m_length);
}
private:
Value m_start = 0;
Value m_length = 0;
};
/// This function merges elements that overlap or touch into single elements
/// and stores the resulting elements into the destination range starting at d_first
///
/// Function assumes the input range is sorted
template <typename InputIt, typename OutputIt>
OutputIt merge_ranges(InputIt first, InputIt last, OutputIt d_first)
{
using Elem = typename InputIt::value_type;
InputIt i = first;
while (first != last) {
Elem e = *first;
++first;
while (first != last && e.canCombine(*first)) {
if (first->end() > e.end())
e.setEnd(first->end());
++first;
}
d_first = e;
}
return d_first;
}
//template <typename Container, typename RangeType>
//void insert(Container cont, RangeType range)
//{
// auto ipos = std::lower_bound(begin(cont), end(cont), range);
// // Maybe it can be combined with previous
// if (ipos != begin(cont)) {
// auto prev = ipos;
// --prev;
// if (prev->canCombine(range))
// prev->setEnd(range->end());
// }
// else {
// // We need to either insert it or combine with the next
// if (ipos != end(cont) && range.canCombine(*ipos)) {
// // remember where end of ipos range was
// auto e = ipos->end();
// if (e > range.end())
// range.setEnd(e);
// // We cannot modify element in set so we remove one element and add a new one
// ipos = cont.erase(ipos);
// cont.insert(ipos, range);
// }
// else {
// cont->insert(ipos, range);
// }
// }
//}
#endif // INTEGERRANGE_H

View file

@ -59,7 +59,8 @@ HEADERS += PasswordManager.h \
SqlAstSelectListEntry.h \ SqlAstSelectListEntry.h \
SqlAstSelect.h \ SqlAstSelect.h \
SqlAstExpression.h \ SqlAstExpression.h \
std_utils.h std_utils.h \
IntegerRange.h
unix { unix {
target.path = /usr/lib target.path = /usr/lib

View file

@ -13,7 +13,9 @@
#include <QFuture> #include <QFuture>
#include <QFutureWatcher> #include <QFutureWatcher>
#include "Pgsql_oids.h" #include "Pgsql_oids.h"
#include "Pgsql_PgException.h"
#include "Pgsql_Params.h" #include "Pgsql_Params.h"
#include "Pgsql_Transaction.h"
#include <string> #include <string>
#include "ScopeGuard.h" #include "ScopeGuard.h"
@ -94,7 +96,6 @@ CrudModel::Value CrudModel::getData(const QModelIndex &index) const
int col = index.column(); int col = index.column();
auto row_mapping = m_rowMapping[grid_row]; auto row_mapping = m_rowMapping[grid_row];
const int last_row = rowCount() - 1; const int last_row = rowCount() - 1;
//Oid o = m_roData->type(col); //Oid o = m_roData->type(col);
@ -225,7 +226,7 @@ void CrudModel::connectionStateChanged(ASyncDBConnection::State state)
Qt::ItemFlags CrudModel::flags(const QModelIndex &) const Qt::ItemFlags CrudModel::flags(const QModelIndex &) const
{ {
Qt::ItemFlags flags = Qt::ItemIsSelectable + Qt::ItemIsEnabled; Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
if (m_primaryKey) { if (m_primaryKey) {
flags |= Qt::ItemIsEditable; flags |= Qt::ItemIsEditable;
} }
@ -508,16 +509,12 @@ void CrudModel::appendNewRow()
endInsertRows(); endInsertRows();
} }
void CrudModel::removeRows()
{
// determine selection
// remove selected rows
}
bool CrudModel::removeRows(int row, int count, const QModelIndex &parent) std::tuple<bool, QString> CrudModel::removeRows(const std::set<IntegerRange<int>> &row_ranges)
{ {
if (m_rowMapping.empty()) return false; if (row_ranges.empty()) return { true, "" };
if (row_ranges.rbegin()->end() > m_rowMapping.size()) return { false, "Range error" };
// When removing rows there is no direct mapping anymore between the rows in the grid // When removing rows there is no direct mapping anymore between the rows in the grid
// and the rows in m_roData // and the rows in m_roData
@ -527,30 +524,41 @@ bool CrudModel::removeRows(int row, int count, const QModelIndex &parent)
// 1. Get PKEY and remove that row from table // 1. Get PKEY and remove that row from table
try {
Pgsql::Connection db_update_conn; Pgsql::Connection db_update_conn;
auto dbconfig = m_database->config(); auto dbconfig = m_database->config();
bool res = db_update_conn.connect(dbconfig.getKeywords(), dbconfig.getValues(), false); bool res = db_update_conn.connect(dbconfig.getKeywords(), dbconfig.getValues(), false);
if (!res) { if (!res) {
return false; return { false, "Cannot connect to the database" };
} }
// First delete rows in table // First delete rows in table
QString delete_statement = createDeleteStatement(); QString delete_statement = createDeleteStatement();
{
db_update_conn.query("BEGIN;"); db_update_conn.query("BEGIN;");
for (int current_row = row; current_row < row + count; ++current_row) { 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&& mapping = m_rowMapping[static_cast<size_t>(current_row)];
auto params = getPKeyParamsForRow(mapping.rowKey); auto params = getPKeyParamsForRow(mapping.rowKey);
if (!params.empty()) { if (!params.empty()) {
// Execute DELETE // Execute DELETE
auto result = db_update_conn.queryParam(delete_statement, params); db_update_conn.queryParam(delete_statement, params);
} }
} }
db_update_conn.query("COMMIT;"); }
tx.commit();
// If something goes wrong after this commit we should reload contents of model
}
// Then from model // Then from model
{ {
beginRemoveRows(parent, row, row);
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(); }; SCOPE_EXIT { endRemoveRows(); };
for (int current_row = row; current_row < row + count; ++current_row) { for (int current_row = range.start(); current_row < range.end(); ++current_row) {
auto&& mapping = m_rowMapping[static_cast<size_t>(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 the row is in modified it can be removed from modified
@ -561,8 +569,24 @@ bool CrudModel::removeRows(int row, int count, const QModelIndex &parent)
} }
// remove the rows from m_rowMapping // remove the rows from m_rowMapping
auto first = m_rowMapping.begin() + row; auto first = m_rowMapping.begin() + range.start();
m_rowMapping.erase(first, first + count); 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()) };
} }
return true;
} }
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;
}

View file

@ -5,11 +5,13 @@
#include "ASyncDBConnection.h" #include "ASyncDBConnection.h"
#include "Pgsql_Connection.h" #include "Pgsql_Connection.h"
#include "IntegerRange.h"
#include "PgClass.h" #include "PgClass.h"
#include "PgConstraint.h" #include "PgConstraint.h"
#include "Pgsql_Connection.h" #include "Pgsql_Connection.h"
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
#include <optional> #include <optional>
@ -73,9 +75,9 @@ public:
void loadData(); void loadData();
std::tuple<bool, QString> removeRows(const std::set<IntegerRange<int>> &row_ranges);
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
/// Removes selected rows
void removeRows();
public slots: public slots:
virtual bool submit() override; virtual bool submit() override;
virtual void revert() override; virtual void revert() override;

View file

@ -4,7 +4,12 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "ResultTableModelUtil.h" #include "ResultTableModelUtil.h"
#include "PgLabItemDelegate.h" #include "PgLabItemDelegate.h"
#include "IntegerRange.h"
#include <QDebug>
#include <QMenu> #include <QMenu>
#include <QMessageBox>
#include <iterator>
#include <set>
CrudTab::CrudTab(MainWindow *parent) CrudTab::CrudTab(MainWindow *parent)
@ -22,7 +27,8 @@ CrudTab::CrudTab(MainWindow *parent)
m_crudModel = new CrudModel(parent); m_crudModel = new CrudModel(parent);
ui->tableView->setModel(m_crudModel); ui->tableView->setModel(m_crudModel);
ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection); ui->tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->tableView->addAction(ui->actionRemove_rows); ui->tableView->addAction(ui->actionRemove_rows);
auto horizontal_header = ui->tableView->horizontalHeader(); auto horizontal_header = ui->tableView->horizontalHeader();
@ -30,9 +36,6 @@ CrudTab::CrudTab(MainWindow *parent)
connect(horizontal_header, &QHeaderView::customContextMenuRequested, connect(horizontal_header, &QHeaderView::customContextMenuRequested,
this, &CrudTab::headerCustomContextMenu); this, &CrudTab::headerCustomContextMenu);
//auto selection_model = ui->tableView->selectionModel();
// connect(ui->tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
// &CrudTab::tableView_currentRowChanged);
} }
CrudTab::~CrudTab() CrudTab::~CrudTab()
@ -44,21 +47,9 @@ void CrudTab::setConfig(std::shared_ptr<OpenDatabase> db, const PgClass &table)
{ {
m_db = db; m_db = db;
m_table = table; m_table = table;
// m_catalog = cat;
// m_tablesModel->setCatalog(cat);
// ui->tableListTable->resizeColumnsToContents();
// m_namespaceFilterWidget->init(cat->namespaces());
// auto highlighter = new SqlSyntaxHighlighter(ui->constraintSqlEdit->document());
// highlighter->setTypes(*cat->types());
m_crudModel->setConfig(db, table); m_crudModel->setConfig(db, table);
} }
//void CrudTab::tableView_currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
//{
//}
void CrudTab::refresh() void CrudTab::refresh()
{ {
m_crudModel->loadData(); m_crudModel->loadData();
@ -66,14 +57,37 @@ void CrudTab::refresh()
void CrudTab::on_actionRemove_rows_triggered() void CrudTab::on_actionRemove_rows_triggered()
{ {
// determine selection std::set<IntegerRange<int>> row_ranges;
auto selection = ui->tableView->selectionModel()->selection();
for (auto range : selection) {
row_ranges.emplace(range.top(), range.height());
}
std::set<IntegerRange<int>> merged_ranges;
merge_ranges(row_ranges.begin(), row_ranges.end(), std::inserter(merged_ranges, merged_ranges.begin()));
// ui->tableView->currentIndex() QString msg = tr("Are you certain you want to remove the following row(s)?");
// selectionModel()-> msg += "\n";
bool first = true;
for (auto range : merged_ranges) {
if (first) first = false;
else msg += ", ";
//ui->tableView->selectionModel()->selectedRows() auto s = range.start() + 1, e = range.end();
m_crudModel->removeRow(ui->tableView->currentIndex().row()); if (s == e)
//removeRows(); msg += QString("%1").arg(s);
else
msg += QString("%1 through %2").arg(s).arg(e);
msg += " ";
}
auto res = QMessageBox::question(this, "pgLab", msg, QMessageBox::Yes, QMessageBox::No);
if (res == QMessageBox::Yes) {
auto [res, msg] = m_crudModel->removeRows(merged_ranges);
if (!res) {
QMessageBox::critical(this, "pgLab", msg, QMessageBox::Close);
}
}
} }
std::vector<QAction*> CrudTab::getToolbarActions() std::vector<QAction*> CrudTab::getToolbarActions()
@ -83,6 +97,8 @@ std::vector<QAction*> CrudTab::getToolbarActions()
action->setShortcut(QKeySequence(Qt::Key_F5)); action->setShortcut(QKeySequence(Qt::Key_F5));
connect(action, &QAction::triggered, this, &CrudTab::refresh); connect(action, &QAction::triggered, this, &CrudTab::refresh);
actions.push_back(action); actions.push_back(action);
actions.push_back(ui->actionRemove_rows);
} }
return actions; return actions;
} }

View file

@ -3,10 +3,21 @@
#include <Qt> #include <Qt>
/// Returned by a model when asked for CustomReferencedTypeRole
///
/// Models will probably only be asked this for columns for which they returned
/// Oid_Oid for the CustomDataTypeRole
enum class ReferencedType {
PgType,
PgNamespace,
PgRole
};
enum CustomDataRole { enum CustomDataRole {
CustomDataTypeRole = Qt::UserRole, CustomDataTypeRole = Qt::UserRole, ///< Requist the basic type of the value
CustomReferencedTypeRole, ///<
// Add other enum before this one is we might want to have multiple hidden values // Add other enum before this one is we might want to have multiple hidden values
FirstHiddenValue, FirstHiddenValue, ///< Used to request value from a model which is not handed to the view
}; };
#endif // CUSTOMDATAROLE_H #endif // CUSTOMDATAROLE_H

View file

@ -1,5 +1,6 @@
#include "Pgsql_Connection.h" #include "Pgsql_Connection.h"
#include "Pgsql_declare.h" #include "Pgsql_declare.h"
#include "Pgsql_PgException.h"
#include "Pgsql_Params.h" #include "Pgsql_Params.h"
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
@ -107,6 +108,7 @@ std::string Connection::getErrorMessage() const
Result Connection::query(const char * command) Result Connection::query(const char * command)
{ {
PGresult *result = PQexec(conn, command); PGresult *result = PQexec(conn, command);
throwError(result);
return Result(result); return Result(result);
} }
@ -114,6 +116,7 @@ Result Connection::queryParam(const char * command, const Params &params)
{ {
PGresult *result = PQexecParams(conn, command, params.size(), params.types(), PGresult *result = PQexecParams(conn, command, params.size(), params.types(),
params.values(), params.lengths(), params.formats(), 0); params.values(), params.lengths(), params.formats(), 0);
throwError(result);
return Result(result); return Result(result);
} }
@ -142,6 +145,7 @@ std::shared_ptr<Result> Connection::getResult()
{ {
PGresult *r = PQgetResult(conn); PGresult *r = PQgetResult(conn);
if (r) { if (r) {
throwError(r);
return std::make_shared<Result>(r); return std::make_shared<Result>(r);
} }
else { else {
@ -225,3 +229,32 @@ QString Connection::getDBName() const
{ {
return QString::fromUtf8(PQdb(conn)); return QString::fromUtf8(PQdb(conn));
} }
void Connection::throwError(PGresult *result) const
{
auto state = PQresultStatus(result);
if (state == PGRES_BAD_RESPONSE) {
// communication problem
}
else if (state == PGRES_FATAL_ERROR) {
auto details = Pgsql::ErrorDetails::createErrorDetailsFromPGresult(result);
throw PgResultError(details);
}
}
//PGRES_EMPTY_QUERY = 0, /* empty query string was executed */
//PGRES_COMMAND_OK, /* a query command that doesn't return
// * anything was executed properly by the
// * backend */
//PGRES_TUPLES_OK, /* a query command that returns tuples was
// * executed properly by the backend, PGresult
// * contains the result tuples */
//PGRES_COPY_OUT, /* Copy Out data transfer in progress */
//PGRES_COPY_IN, /* Copy In data transfer in progress */
//PGRES_BAD_RESPONSE, /* an unexpected response was recv'd from the
// * backend */
//PGRES_NONFATAL_ERROR, /* notice or warning message */
//PGRES_FATAL_ERROR, /* query failed */
//PGRES_COPY_BOTH, /* Copy In/Out data transfer in progress */
//PGRES_SINGLE_TUPLE /* single tuple from larger resultset */

View file

@ -114,6 +114,7 @@ namespace Pgsql {
PGconn *conn = nullptr; PGconn *conn = nullptr;
std::function<void(const PGresult *)> notifyReceiver; std::function<void(const PGresult *)> notifyReceiver;
void throwError(PGresult *result) const;
static void notifyReceiveFunc(void *arg, const PGresult *result); static void notifyReceiveFunc(void *arg, const PGresult *result);
}; };

View file

@ -1 +1,54 @@
#include "Pgsql_PgException.h" #include "Pgsql_PgException.h"
namespace Pgsql {
ResultCode::ResultCode(std::string result_code)
: m_resultCode(std::move(result_code))
{
assert(m_resultCode.length() == 5);
}
std::string ResultCode::getClass() const
{
return m_resultCode.substr(1,2);
}
/** Helper to easily check the class of the error
*
*/
bool ResultCode::isClass(const std::string_view cls)
{
return m_resultCode.compare(1, 2, cls);
}
const std::string& ResultCode::getSpecific() const
{
return m_resultCode;
}
PgException::PgException(const char *msg)
: std::runtime_error(msg)
{}
PgException::PgException(std::string msg)
: std::runtime_error(msg)
{}
PgResultError::PgResultError(const Pgsql::ErrorDetails &details)
: PgException(details.errorMessage)
, m_details(details)
{}
ResultCode PgResultError::getResultCode() const
{
return ResultCode(m_details.state);
}
const Pgsql::ErrorDetails& PgResultError::details() const
{
return m_details;
}
}

View file

@ -1,6 +1,7 @@
#ifndef PGEXCEPTION_H #ifndef PGEXCEPTION_H
#define PGEXCEPTION_H #define PGEXCEPTION_H
#include "Pgsql_ErrorDetails.h"
#include <cassert> #include <cassert>
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
@ -9,68 +10,39 @@ namespace Pgsql {
class ResultCode { class ResultCode {
public: public:
explicit ResultCode(std::string result_code) explicit ResultCode(std::string result_code);
: m_resultCode(std::move(result_code)) std::string getClass() const;
{ bool isClass(const std::string_view cls);
assert(m_resultCode.length() == 5); const std::string& getSpecific() const;
}
std::string getClass() const
{
return m_resultCode.substr(1,2);
}
/** Helper to easily check the class of the error
*
*/
bool isClass(const std::string_view cls)
{
return m_resultCode.compare(1, 2, cls);
}
const std::string& getSpecific() const
{
return m_resultCode;
}
private: private:
std::string m_resultCode; std::string m_resultCode;
}; };
class PgException: public std::runtime_error { class PgException: public std::runtime_error {
public: public:
PgException(const char *msg) PgException(const char *msg);
: std::runtime_error(msg) PgException(std::string msg);
{}
PgException(std::string msg)
: std::runtime_error(msg)
{}
private: private:
}; };
class PgResultError: public PgException { class PgResultError: public PgException {
public: public:
PgResultError(std::string msg, std::string result_code) PgResultError(const Pgsql::ErrorDetails &details);
: PgException(std::move(msg)) ResultCode getResultCode() const;
, m_resultCode(result_code) const Pgsql::ErrorDetails& details() const;
{}
ResultCode getResultCode() const { return m_resultCode; }
private: private:
ResultCode m_resultCode; Pgsql::ErrorDetails m_details;
}; };
class PgConnectionError: public PgResultError { // class PgConnectionError: public PgException {
public: // public:
PgConnectionError(const std::string &msg, std::string result_code) // PgConnectionError(const std::string &msg, std::string result_code)
: PgResultError(msg, std::move(result_code)) // : PgResultError(msg, std::move(result_code))
{} // {}
private: // private:
}; // };