Allow any number of changes to the table schema in one alterTable call

In the Edit Table dialog we used to call our alterTable function (which
works around SQLite's missing full ALTER TABLE support by - besided
other things - copying all the data of the table) for pretty much every
change immediately. This was taking a lot of time for larger tables.

Our alterTable function allowed any number of changes if they affect
only one field of the table at once. So we could have reduced the number
of calls a lot by just using that capability. Instead however, this
commit improves the alterTable function to make possible transforming a
table completely in just one call. It does so by taking the new table
schema and using that without further modification. It also takes a new
parameter to keep track of what column in the old table becomes what
column in the new table, so the data can be preserved.

This commit obviously also changes the Edit Table dialog to make proper
use of the new features. This means that whatever changes you make to a
table, you will only have to wait once until for the alterTable call,
and that's when clicking the OK button.

See issue #1444.
This commit is contained in:
Martin Kleusberg
2018-10-21 22:09:03 +02:00
parent 37a5645bf5
commit 9e36f21112
6 changed files with 319 additions and 266 deletions

View File

@@ -37,6 +37,10 @@ EditTableDialog::EditTableDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
m_table = *(pdb.getObjectByName<sqlb::Table>(curTable));
ui->labelEditWarning->setVisible(!m_table.fullyParsed());
// Initialise the list of tracked columns for table layout changes
for(const auto& field : m_table.fields)
trackColumns[field.name()] = field.name();
// Set without rowid checkbox and schema dropdown. No need to trigger any events here as we're only loading a table exactly as it is stored by SQLite, so no need
// for error checking etc.
ui->checkWithoutRowid->blockSignals(true);
@@ -57,9 +61,6 @@ EditTableDialog::EditTableDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
// And create a savepoint
pdb.setSavepoint(m_sRestorePointName);
// Check now if foreign keys are enabled so we don't have to query this later again and again
m_bForeignKeysEnabled = (pdb.getPragma("foreign_keys") == "1");
// Update UI
ui->editTableName->setText(curTable.name());
updateColumnWidth();
@@ -170,14 +171,11 @@ void EditTableDialog::accept()
} else {
// Editing of old table
// Rename table if necessary
if(ui->editTableName->text() != curTable.name())
// Apply all changes to the actual table in the database
if(!pdb.alterTable(curTable, m_table, trackColumns, ui->comboSchema->currentText()))
{
if(!pdb.renameTable(ui->comboSchema->currentText(), curTable.name(), ui->editTableName->text()))
{
QMessageBox::warning(this, QApplication::applicationName(), pdb.lastError());
return;
}
QMessageBox::warning(this, QApplication::applicationName(), pdb.lastError());
return;
}
}
@@ -214,13 +212,8 @@ void EditTableDialog::checkInput()
const auto& fields = m_table.fields;
for(const sqlb::Field& f : fields) {
auto fk = std::dynamic_pointer_cast<sqlb::ForeignKeyClause>(m_table.constraint({f.name()}, sqlb::Constraint::ForeignKeyConstraintType));
if(fk) {
if (oldTableName == fk->table()) {
fk->setTable(normTableName);
if(!m_bForeignKeysEnabled)
pdb.alterTable(curTable, m_table, f.name(), &f, 0);
}
}
if(fk && oldTableName == fk->table())
fk->setTable(normTableName);
}
populateFields();
@@ -238,16 +231,15 @@ void EditTableDialog::updateTypes(QObject *object)
QString type = typeBox->currentText();
QString column = typeBox->property("column").toString();
size_t index;
for(index=0; index < m_table.fields.size(); ++index)
for(size_t index=0; index < m_table.fields.size(); ++index)
{
if(m_table.fields.at(index).name() == column)
{
m_table.fields.at(index).setType(type);
break;
}
}
m_table.fields.at(index).setType(type);
if(!m_bNewTable)
pdb.alterTable(curTable, m_table, column, &m_table.fields[index]);
checkInput();
}
}
@@ -272,7 +264,6 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
if(index < static_cast<int>(m_table.fields.size()))
{
sqlb::Field& field = m_table.fields.at(index);
bool callRenameColumn = false;
QString oldFieldName = field.name();
switch(column)
@@ -328,8 +319,16 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
field.setName(item->text(column));
m_table.renameKeyInAllConstraints(oldFieldName, item->text(column));
qobject_cast<QComboBox*>(ui->treeWidget->itemWidget(item, kType))->setProperty("column", item->text(column));
// Update the field name in the map of old column names to new column names
if(!m_bNewTable)
callRenameColumn = true;
{
for(const auto& key : trackColumns)
{
if(trackColumns[key] == oldFieldName)
trackColumns[key] = field.name();
}
}
} break;
case kType:
// see updateTypes() SLOT
@@ -362,8 +361,6 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
} else {
item->setCheckState(kAutoIncrement, Qt::Unchecked);
}
if(!m_bNewTable)
callRenameColumn = true;
}
break;
case kNotNull:
@@ -395,8 +392,6 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
}
}
field.setNotNull(item->checkState(column) == Qt::Checked);
if(!m_bNewTable)
callRenameColumn = true;
}
break;
case kAutoIncrement:
@@ -446,9 +441,6 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
}
}
field.setAutoIncrement(ischecked);
if(!m_bNewTable)
callRenameColumn = true;
}
break;
case kUnique:
@@ -484,9 +476,6 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
}
}
field.setUnique(item->checkState(column) == Qt::Checked);
if(!m_bNewTable)
callRenameColumn = true;
}
break;
case kDefault:
@@ -517,28 +506,15 @@ void EditTableDialog::itemChanged(QTreeWidgetItem *item, int column)
}
}
field.setDefaultValue(new_value);
if(!m_bNewTable)
callRenameColumn = true;
}
break;
case kCheck:
field.setCheck(item->text(column));
if(!m_bNewTable)
callRenameColumn = true;
break;
case kForeignKey:
// handled in delegate
if(!m_bNewTable)
callRenameColumn = true;
break;
}
if(callRenameColumn)
{
if(!pdb.alterTable(curTable, m_table, oldFieldName, &field))
QMessageBox::warning(this, qApp->applicationName(), tr("Modifying this column failed. Error returned from database:\n%1").arg(pdb.lastError()));
}
}
checkInput();
@@ -590,9 +566,9 @@ void EditTableDialog::addField()
// add field to table object
m_table.fields.emplace_back(tbitem->text(kName), typeBox->currentText());
// Actually add the new column to the table if we're editing an existing table
// Add the new column to the list of tracked columns to indicate it has been added
if(!m_bNewTable)
pdb.addColumn(curTable, m_table.fields.back());
trackColumns.insert(QString(), tbitem->text(kName));
checkInput();
}
@@ -603,33 +579,27 @@ void EditTableDialog::removeField()
if(!ui->treeWidget->currentItem())
return;
// Are we creating a new table or editing an old one?
if(m_bNewTable)
// If we are editing an existing table, ask the user for confirmation
if(!m_bNewTable)
{
// Creating a new one
// Just delete that item. At this point there is no DB table to edit or data to be lost anyway
m_table.fields.erase(m_table.fields.begin() + ui->treeWidget->indexOfTopLevelItem(ui->treeWidget->currentItem()));
m_table.removeKeyFromAllConstraints(ui->treeWidget->currentItem()->text(kName));
delete ui->treeWidget->currentItem();
} else {
// Editing an old one
// Ask user whether he really wants to delete that column
QString msg = tr("Are you sure you want to delete the field '%1'?\nAll data currently stored in this field will be lost.").arg(ui->treeWidget->currentItem()->text(0));
if(QMessageBox::warning(this, QApplication::applicationName(), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)
if(QMessageBox::warning(this, QApplication::applicationName(), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No)
return;
// Update the map of tracked columns to indicate the column is deleted
QString name = ui->treeWidget->currentItem()->text(0);
for(const auto& key : trackColumns)
{
if(!pdb.alterTable(curTable, m_table, ui->treeWidget->currentItem()->text(0), nullptr))
{
QMessageBox::warning(nullptr, QApplication::applicationName(), pdb.lastError());
} else {
//relayout
m_table = *(pdb.getObjectByName<sqlb::Table>(curTable));
populateFields();
}
if(trackColumns[key] == name)
trackColumns[key] = QString();
}
}
// Just delete that item. At this point there is no DB table to edit or data to be lost anyway
m_table.fields.erase(m_table.fields.begin() + ui->treeWidget->indexOfTopLevelItem(ui->treeWidget->currentItem()));
m_table.removeKeyFromAllConstraints(ui->treeWidget->currentItem()->text(kName));
delete ui->treeWidget->currentItem();
checkInput();
}
@@ -665,54 +635,27 @@ void EditTableDialog::moveCurrentField(bool down)
int currentRow = ui->treeWidget->currentIndex().row();
int newRow = currentRow + (down ? 1 : -1);
// Are we creating a new table or editing an old one?
if(m_bNewTable)
{
// Creating a new one
// Save the combobox first by making a copy
QComboBox* oldCombo = qobject_cast<QComboBox*>(ui->treeWidget->itemWidget(ui->treeWidget->topLevelItem(currentRow), kType));
QComboBox* newCombo = new QComboBox(ui->treeWidget);
newCombo->setProperty("column", oldCombo->property("column"));
newCombo->installEventFilter(this);
connect(newCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTypes()));
newCombo->setEditable(true);
for(int i=0; i < oldCombo->count(); ++i)
newCombo->addItem(oldCombo->itemText(i));
newCombo->setCurrentIndex(oldCombo->currentIndex());
// Save the combobox first by making a copy
QComboBox* oldCombo = qobject_cast<QComboBox*>(ui->treeWidget->itemWidget(ui->treeWidget->topLevelItem(currentRow), kType));
QComboBox* newCombo = new QComboBox(ui->treeWidget);
newCombo->setProperty("column", oldCombo->property("column"));
newCombo->installEventFilter(this);
connect(newCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTypes()));
newCombo->setEditable(true);
for(int i=0; i < oldCombo->count(); ++i)
newCombo->addItem(oldCombo->itemText(i));
newCombo->setCurrentIndex(oldCombo->currentIndex());
// Now, just remove the item and insert it at it's new position, then restore the combobox
QTreeWidgetItem* item = ui->treeWidget->takeTopLevelItem(currentRow);
ui->treeWidget->insertTopLevelItem(newRow, item);
ui->treeWidget->setItemWidget(item, kType, newCombo);
// Now, just remove the item and insert it at it's new position, then restore the combobox
QTreeWidgetItem* item = ui->treeWidget->takeTopLevelItem(currentRow);
ui->treeWidget->insertTopLevelItem(newRow, item);
ui->treeWidget->setItemWidget(item, kType, newCombo);
// Select the old item at its new position
ui->treeWidget->setCurrentIndex(ui->treeWidget->currentIndex().sibling(newRow, 0));
// Select the old item at its new position
ui->treeWidget->setCurrentIndex(ui->treeWidget->currentIndex().sibling(newRow, 0));
// Finally update the table SQL
std::swap(m_table.fields[newRow], m_table.fields[currentRow]);
} else {
// Editing an old one
// Move the actual column
if(!pdb.alterTable(
curTable,
m_table,
ui->treeWidget->currentItem()->text(0),
&m_table.fields[ui->treeWidget->indexOfTopLevelItem(ui->treeWidget->currentItem())],
(down ? 1 : -1)
))
{
QMessageBox::warning(nullptr, QApplication::applicationName(), pdb.lastError());
} else {
// Reload table SQL
m_table = *(pdb.getObjectByName<sqlb::Table>(curTable));
populateFields();
// Select old item at new position
ui->treeWidget->setCurrentIndex(ui->treeWidget->indexAt(QPoint(1, 1)).sibling(newRow, 0));
}
}
// Finally update the table SQL
std::swap(m_table.fields[newRow], m_table.fields[currentRow]);
// Update the SQL preview
updateSqlText();
@@ -748,33 +691,10 @@ void EditTableDialog::setWithoutRowid(bool without_rowid)
// Update the SQL preview
updateSqlText();
// Update table if we're editing an existing table
if(!m_bNewTable)
{
if(!pdb.alterTable(curTable, m_table, QString(), nullptr, 0))
{
QMessageBox::warning(this, QApplication::applicationName(),
tr("Setting the rowid column for the table failed. Error message:\n%1").arg(pdb.lastError()));
}
}
}
void EditTableDialog::changeSchema(const QString& schema)
void EditTableDialog::changeSchema(const QString& /*schema*/)
{
// Update the SQL preview
updateSqlText();
// Update table if we're editing an existing table
if(!m_bNewTable)
{
if(pdb.alterTable(curTable, m_table, QString(), nullptr, 0, schema))
{
// Save the new schema name to use it from now on
curTable.setSchema(schema);
} else {
QMessageBox::warning(this, QApplication::applicationName(), tr("Changing the table schema failed. Error message:\n%1").arg(pdb.lastError()));
ui->comboSchema->setCurrentText(curTable.schema()); // Set it back to the original schema
}
}
}