From 21ee1f2703967b6caa6c07d6e0cfdca0b5c5d25f Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Fri, 12 May 2017 18:17:50 +0200 Subject: [PATCH] Allow updating views This adds a new context menu option that allows unlocking views for updating. This requires appropriate triggers to be in place and the user to type in a column name that can be used as a 'primary key' for the view. By default views are still locked from editing. Inserting into and deleting from views isn't supported yet. See issue #141. --- src/MainWindow.cpp | 80 +++++++++++++++++++++++++++++++++++++--- src/MainWindow.h | 5 +++ src/MainWindow.ui | 28 ++++++++++++++ src/sqlitedb.cpp | 4 +- src/sqlitedb.h | 2 +- src/sqlitetablemodel.cpp | 19 +++++++++- src/sqlitetablemodel.h | 7 +++- 7 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2eb105a1..65dbbde5 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -156,6 +156,7 @@ void MainWindow::init() popupBrowseDataHeaderMenu->addAction(ui->actionShowRowidColumn); popupBrowseDataHeaderMenu->addAction(ui->actionBrowseTableEditDisplayFormat); popupBrowseDataHeaderMenu->addAction(ui->actionSetTableEncoding); + popupBrowseDataHeaderMenu->addAction(ui->actionUnlockViewEditing); popupBrowseDataHeaderMenu->addSeparator(); popupBrowseDataHeaderMenu->addAction(ui->actionSetAllTablesEncoding); @@ -453,6 +454,9 @@ void MainWindow::populateTable() // Hide rowid column. Needs to be done before the column widths setting because of the workaround in there showRowidColumn(false); + // Enable editing in general, but lock view editing + unlockViewEditing(false); + // Column widths for(int i=1;icolumnCount();i++) ui->dataTable->setColumnWidth(i, ui->dataTable->horizontalHeader()->defaultSectionSize()); @@ -497,6 +501,9 @@ void MainWindow::populateTable() // because of the filter row generation. showRowidColumn(storedData.showRowid); + // Enable editing in general and (un)lock view editing depending on the settings + unlockViewEditing(!storedData.unlockViewPk.isEmpty(), storedData.unlockViewPk); + // Column widths for(auto widthIt=storedData.columnWidths.constBegin();widthIt!=storedData.columnWidths.constEnd();++widthIt) ui->dataTable->setColumnWidth(widthIt.key(), widthIt.value()); @@ -516,11 +523,15 @@ void MainWindow::populateTable() plotDock->updatePlot(m_browseTableModel, &browseTableSettings[ui->comboBrowseTable->currentText()], true, false); } - // Activate the add and delete record buttons and editing only if a table has been selected - bool editable = db.getObjectByName(tablename)->type() == sqlb::Object::Types::Table && !db.readOnly(); - ui->buttonNewRecord->setEnabled(editable); - ui->buttonDeleteRecord->setEnabled(editable); - ui->dataTable->setEditTriggers(editable ? QAbstractItemView::SelectedClicked | QAbstractItemView::AnyKeyPressed | QAbstractItemView::EditKeyPressed : QAbstractItemView::NoEditTriggers); + // Show/hide menu options depending on whether this is a table or a view + if(db.getObjectByName(ui->comboBrowseTable->currentText())->type() == sqlb::Object::Table) + { + // Table + ui->actionUnlockViewEditing->setVisible(false); + } else { + // View + ui->actionUnlockViewEditing->setVisible(true); + } // Set the recordset label setRecordsetLabel(); @@ -1438,6 +1449,19 @@ void MainWindow::activateFields(bool enable) ui->actionSave_Remote->setEnabled(enable); } +void MainWindow::enableEditing(bool enable_edit, bool enable_insertdelete) +{ + // Don't enable anything if this is a read only database + bool edit = enable_edit && !db.readOnly(); + bool insertdelete = enable_insertdelete && !db.readOnly(); + + // Apply settings + ui->buttonNewRecord->setEnabled(insertdelete); + ui->buttonDeleteRecord->setEnabled(insertdelete); + ui->dataTable->setEditTriggers(edit ? QAbstractItemView::SelectedClicked | QAbstractItemView::AnyKeyPressed | QAbstractItemView::EditKeyPressed : QAbstractItemView::NoEditTriggers); + +} + void MainWindow::browseTableHeaderClicked(int logicalindex) { // Abort if there is more than one column selected because this tells us that the user pretty sure wants to do a range selection instead of sorting data @@ -1939,6 +1963,7 @@ bool MainWindow::loadProject(QString filename, bool readOnly) ui->dataTable->sortByColumn(browseTableSettings[ui->comboBrowseTable->currentText()].sortOrderIndex, browseTableSettings[ui->comboBrowseTable->currentText()].sortOrderMode); showRowidColumn(browseTableSettings[ui->comboBrowseTable->currentText()].showRowid); + unlockViewEditing(!browseTableSettings[ui->comboBrowseTable->currentText()].unlockViewPk.isEmpty(), browseTableSettings[ui->comboBrowseTable->currentText()].unlockViewPk); xml.skipCurrentElement(); } } @@ -2366,3 +2391,48 @@ void MainWindow::fileOpenReadOnly() // Redirect to 'standard' fileOpen(), with the read only flag set fileOpen(QString(), false, true); } + +void MainWindow::unlockViewEditing(bool unlock, QString pk) +{ + QString currentTable = ui->comboBrowseTable->currentText(); + + // If this isn't a view just unlock editing and return + if(db.getObjectByName(currentTable)->type() != sqlb::Object::View) + { + m_browseTableModel->setPseudoPk(QString()); + enableEditing(true, true); + return; + } + + // If the view gets unlocked for editing and we don't have a 'primary key' for this view yet, then ask for one + if(unlock && pk.isEmpty()) + { + while(true) + { + // Ask for a PK + pk = QInputDialog::getText(this, qApp->applicationName(), tr("Please enter a pseudo-primary key in order to enable editing on this view. " + "This should be the name of a unique column in the view.")); + + // Cancelled? + if(pk.isEmpty()) + return; + + // Do some basic testing of the input and if the input appears to be good, go on + if(db.executeSQL(QString("SELECT %1 FROM %2 LIMIT 1;").arg(sqlb::escapeIdentifier(pk)).arg(sqlb::escapeIdentifier(currentTable)), false, true)) + break; + } + } else if(!unlock) { + // Locking the view is done by unsetting the pseudo-primary key + pk.clear(); + } + + // (De)activate editing + enableEditing(unlock, false); + m_browseTableModel->setPseudoPk(pk); + + // Update checked status of the popup menu action + ui->actionUnlockViewEditing->setChecked(unlock); + + // Save settings for this table + browseTableSettings[currentTable].unlockViewPk = pk; +} diff --git a/src/MainWindow.h b/src/MainWindow.h index f0fe0a79..8fe28d9f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -33,6 +33,7 @@ struct BrowseDataTableSettings QString encoding; QString plotXAxis; QMap plotYAxes; + QString unlockViewPk; friend QDataStream& operator<<(QDataStream& stream, const BrowseDataTableSettings& object) { @@ -45,6 +46,7 @@ struct BrowseDataTableSettings stream << object.encoding; stream << object.plotXAxis; stream << object.plotYAxes; + stream << object.unlockViewPk; return stream; } @@ -68,6 +70,7 @@ struct BrowseDataTableSettings stream >> object.plotXAxis; stream >> object.plotYAxes; + stream >> object.unlockViewPk; return stream; } @@ -154,6 +157,7 @@ private: void setCurrentFile(const QString& fileName); void addToRecentFilesMenu(const QString& filename); void activateFields(bool enable = true); + void enableEditing(bool enable_edit, bool enable_insertdelete); void loadExtensionsFromSettings(); protected: @@ -242,6 +246,7 @@ private slots: void browseDataSetTableEncoding(bool forAllTables = false); void browseDataSetDefaultTableEncoding(); void fileOpenReadOnly(); + void unlockViewEditing(bool unlock, QString pk = QString()); }; #endif diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 646e4c42..0d57273a 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -1779,6 +1779,17 @@ QAction::NoRole + + + true + + + Unlock view editing + + + This unlocks the current view for editing. However, you will need appropriate triggers for editing. + + @@ -2814,6 +2825,22 @@ + + actionUnlockViewEditing + toggled(bool) + MainWindow + unlockViewEditing(bool) + + + -1 + -1 + + + 518 + 314 + + + fileOpen() @@ -2877,5 +2904,6 @@ browseDataFetchAllData() exportTableToJson() fileOpenReadOnly() + unlockViewEditing(bool) diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 6e25e220..e5bae960 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -945,14 +945,14 @@ bool DBBrowserDB::deleteRecords(const QString& table, const QStringList& rowids) return ok; } -bool DBBrowserDB::updateRecord(const QString& table, const QString& column, const QString& rowid, const QByteArray& value, bool itsBlob) +bool DBBrowserDB::updateRecord(const QString& table, const QString& column, const QString& rowid, const QByteArray& value, bool itsBlob, const QString& pseudo_pk) { if (!isOpen()) return false; QString sql = QString("UPDATE %1 SET %2=? WHERE %3='%4';") .arg(sqlb::escapeIdentifier(table)) .arg(sqlb::escapeIdentifier(column)) - .arg(sqlb::escapeIdentifier(getObjectByName(table).dynamicCast()->rowidColumn())) + .arg(sqlb::escapeIdentifier(pseudo_pk.isEmpty() ? getObjectByName(table).dynamicCast()->rowidColumn() : pseudo_pk)) .arg(rowid); logSQL(sql, kLogMsg_App); diff --git a/src/sqlitedb.h b/src/sqlitedb.h index eb22e329..9d8071d1 100644 --- a/src/sqlitedb.h +++ b/src/sqlitedb.h @@ -67,7 +67,7 @@ public: */ QString emptyInsertStmt(const sqlb::Table& t, const QString& pk_value = QString()) const; bool deleteRecords(const QString& table, const QStringList& rowids); - bool updateRecord(const QString& table, const QString& column, const QString& rowid, const QByteArray& value, bool itsBlob); + bool updateRecord(const QString& table, const QString& column, const QString& rowid, const QByteArray& value, bool itsBlob, const QString& pseudo_pk = QString()); bool createTable(const QString& name, const sqlb::FieldVector& structure); bool renameTable(const QString& from_table, const QString& to_table); diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 379aaa0e..3fd533d1 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -31,6 +31,7 @@ void SqliteTableModel::reset() m_mWhere.clear(); m_vDataTypes.clear(); m_vDisplayFormat.clear(); + m_pseudoPk.clear(); } void SqliteTableModel::setChunkSize(size_t chunksize) @@ -341,7 +342,7 @@ bool SqliteTableModel::setTypedData(const QModelIndex& index, bool isBlob, const if(oldValue == newValue && oldValue.isNull() == newValue.isNull()) return true; - if(m_db.updateRecord(m_sTable, m_headers.at(index.column()), m_data[index.row()].at(0), newValue, isBlob)) + if(m_db.updateRecord(m_sTable, m_headers.at(index.column()), m_data[index.row()].at(0), newValue, isBlob, m_pseudoPk)) { // Only update the cache if this row has already been read, if not there's no need to do any changes to the cache if(index.row() < m_data.size()) @@ -767,3 +768,19 @@ bool SqliteTableModel::dropMimeData(const QMimeData* data, Qt::DropAction, int r return false; } + +void SqliteTableModel::setPseudoPk(const QString& pseudoPk) +{ + if(pseudoPk.isEmpty()) + { + m_pseudoPk.clear(); + if(m_headers.size()) + m_headers[0] = "rowid"; + } else { + m_pseudoPk = pseudoPk; + if(m_headers.size()) + m_headers[0] = pseudoPk; + } + + buildQuery(); +} diff --git a/src/sqlitetablemodel.h b/src/sqlitetablemodel.h index 024d48ee..72c3569a 100644 --- a/src/sqlitetablemodel.h +++ b/src/sqlitetablemodel.h @@ -48,9 +48,13 @@ public: bool isBinary(const QModelIndex& index) const; - void setEncoding(QString encoding) { m_encoding = encoding; } + void setEncoding(const QString& encoding) { m_encoding = encoding; } QString encoding() const { return m_encoding; } + // The pseudo-primary key is exclusively for editing views + void setPseudoPk(const QString& pseudoPk); + QString pseudoPk() const { return m_pseudoPk; } + typedef QList QByteArrayList; sqlb::ForeignKeyClause getForeignKeyClause(int column) const; @@ -82,6 +86,7 @@ private: QString m_sQuery; QString m_sTable; + QString m_pseudoPk; int m_iSortColumn; QString m_sSortOrder; QMap m_mWhere;