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.
This commit is contained in:
Martin Kleusberg
2017-05-12 18:17:50 +02:00
parent e404b22205
commit 21ee1f2703
7 changed files with 135 additions and 10 deletions

View File

@@ -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;i<m_browseTableModel->columnCount();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;
}

View File

@@ -33,6 +33,7 @@ struct BrowseDataTableSettings
QString encoding;
QString plotXAxis;
QMap<QString, PlotDock::PlotSettings> 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

View File

@@ -1779,6 +1779,17 @@
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionUnlockViewEditing">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Unlock view editing</string>
</property>
<property name="toolTip">
<string>This unlocks the current view for editing. However, you will need appropriate triggers for editing.</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
@@ -2814,6 +2825,22 @@
</hint>
</hints>
</connection>
<connection>
<sender>actionUnlockViewEditing</sender>
<signal>toggled(bool)</signal>
<receiver>MainWindow</receiver>
<slot>unlockViewEditing(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>518</x>
<y>314</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>fileOpen()</slot>
@@ -2877,5 +2904,6 @@
<slot>browseDataFetchAllData()</slot>
<slot>exportTableToJson()</slot>
<slot>fileOpenReadOnly()</slot>
<slot>unlockViewEditing(bool)</slot>
</slots>
</ui>

View File

@@ -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<sqlb::Table>()->rowidColumn()))
.arg(sqlb::escapeIdentifier(pseudo_pk.isEmpty() ? getObjectByName(table).dynamicCast<sqlb::Table>()->rowidColumn() : pseudo_pk))
.arg(rowid);
logSQL(sql, kLogMsg_App);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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<QByteArray> 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<int, QString> m_mWhere;