From c8cd8586996b065ddd46d2a49f886ea448226627 Mon Sep 17 00:00:00 2001 From: mgrojo Date: Fri, 22 Jan 2021 23:54:26 +0100 Subject: [PATCH] Add "Save Database As..." action The feature is implemented using the SQLite backup API. https://sqlite.org/backup.html This allows saving in-memory databases and saving database files to another file name. --- src/MainWindow.cpp | 28 +++++++++++++++++++++++ src/MainWindow.h | 1 + src/MainWindow.ui | 32 ++++++++++++++++++++++++++ src/sqlitedb.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++ src/sqlitedb.h | 1 + 5 files changed, 118 insertions(+) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 39971ab2..13ec70bc 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -687,6 +687,30 @@ void MainWindow::refreshTableBrowsers(bool force_refresh) QApplication::restoreOverrideCursor(); } +bool MainWindow::fileSaveAs() { + + QString fileName = FileDialog::getSaveFileName( + OpenDatabaseFile, + this, + tr("Choose a database file to save under"), + FileDialog::getSqlDatabaseFileFilter() + ); + // catch situation where user has canceled file selection from dialog + if(!fileName.isEmpty()) { + bool result = db.saveAs(fileName.toStdString()); + if(result) { + setCurrentFile(fileName); + addToRecentFilesMenu(fileName); + } else { + QMessageBox::warning(this, QApplication::applicationName(), + tr("Error while saving the database to the new file.")); + } + return result; + } else { + return false; + } +} + bool MainWindow::fileClose() { // Stop any running SQL statements before closing the database @@ -1415,6 +1439,9 @@ void MainWindow::dbState(bool dirty) { ui->fileSaveAction->setEnabled(dirty); ui->fileRevertAction->setEnabled(dirty); + // Unfortunately, sqlite does not allow to backup the DB while there are pending savepoints, + // so we cannot "Save As" when the DB is dirty. + ui->fileSaveAsAction->setEnabled(!dirty); } void MainWindow::fileSave() @@ -1824,6 +1851,7 @@ void MainWindow::activateFields(bool enable) bool tempDb = db.currentFile() == ":memory:"; ui->fileCloseAction->setEnabled(enable); + ui->fileSaveAsAction->setEnabled(enable); ui->fileAttachAction->setEnabled(enable); ui->fileCompactAction->setEnabled(enable && write); ui->fileExportJsonAction->setEnabled(enable); diff --git a/src/MainWindow.h b/src/MainWindow.h index fcb0eda1..cd845523 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -167,6 +167,7 @@ private slots: void fileNewInMemoryDatabase(); void refreshTableBrowsers(bool force_refresh = false); bool fileClose(); + bool fileSaveAs(); void createTable(); void createIndex(); void compact(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 5ec3b562..6076bce6 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -785,6 +785,7 @@ You can drag SQL statements from an object row and drop them into other applicat + @@ -2240,6 +2241,21 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed This shows the number of rows for each table and view in the database. + + + false + + + + :/icons/db_save:/icons/db_save + + + Save Database &As... + + + Save the current database as a different file + + @@ -2350,6 +2366,22 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed + + fileSaveAsAction + triggered() + MainWindow + fileSaveAs() + + + -1 + -1 + + + 399 + 299 + + + fileCompactAction triggered() diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 4268e45a..9414a23c 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -755,6 +755,62 @@ bool DBBrowserDB::close() return true; } +bool DBBrowserDB::saveAs(const std::string& filename) { + int rc; + sqlite3_backup *pBackup; + sqlite3 *pTo; + + if(!_db) + return false; + + waitForDbRelease(); + + // Open the database file identified by filename. Exit early if this fails + // for any reason. + rc = sqlite3_open(filename.c_str(), &pTo); + if(rc!=SQLITE_OK) { + qWarning() << tr("Cannot open destination file: '%1'").arg(filename.c_str()); + return false; + } else { + // Set up the backup procedure to copy from the "main" database of + // connection _db to the main database of connection pTo. + // If something goes wrong, pBackup will be set to nullptr and an error + // code and message left in connection pTo. + // + // If the backup object is successfully created, call backup_step() + // to copy data from _db to pTo. Then call backup_finish() + // to release resources associated with the pBackup object. If an + // error occurred, then an error code and message will be left in + // connection pTo. If no error occurred, then the error code belonging + // to pTo is set to SQLITE_OK. + // + pBackup = sqlite3_backup_init(pTo, "main", _db, "main"); + if(pBackup == nullptr) { + qWarning() << tr("Cannot backup to file: '%1'. Message: %2").arg(filename.c_str()).arg(sqlite3_errmsg(pTo)); + sqlite3_close(pTo); + return false; + } else { + sqlite3_backup_step(pBackup, -1); + sqlite3_backup_finish(pBackup); + } + rc = sqlite3_errcode(pTo); + } + + if(rc == SQLITE_OK) { + // Close current database and set backup as current + sqlite3_close(_db); + _db = pTo; + curDBFilename = QString::fromStdString(filename); + + return true; + } else { + qWarning() << tr("Cannot backup to file: '%1'. Message: %2").arg(filename.c_str()).arg(sqlite3_errmsg(pTo)); + // Close failed database connection. + sqlite3_close(pTo); + return false; + } +} + DBBrowserDB::db_pointer_type DBBrowserDB::get(const QString& user, bool force_wait) { if(!_db) diff --git a/src/sqlitedb.h b/src/sqlitedb.h index 90b6a36b..f8a68414 100644 --- a/src/sqlitedb.h +++ b/src/sqlitedb.h @@ -80,6 +80,7 @@ public: bool detach(const std::string& attached_as); bool create ( const QString & db); bool close(); + bool saveAs(const std::string& filename); // This returns the SQLite version as well as the SQLCipher if DB4S is compiled with encryption support static void getSqliteVersion(QString& sqlite, QString& sqlcipher);