#include "configimpl.h" #include "sqlhistorymodel.h" #include "ddlhistorymodel.h" #include "services/notifymanager.h" #include "sqlitestudio.h" #include "db/dbsqlite3.h" #include "common/utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include static_qstring(DB_FILE_NAME, "settings3"); static_qstring(CONFIG_DIR_SETTING, "SQLiteStudioConfigDir"); qint64 ConfigImpl::sqlHistoryId = -1; QString ConfigImpl::memoryDbName = QStringLiteral(":memory:"); ConfigImpl::~ConfigImpl() { cleanUp(); } void ConfigImpl::init() { initDbFile(); initTables(); updateConfigDb(); mergeMasterConfig(); sqlite3Version = db->exec("SELECT sqlite_version()")->getSingleCell().toString(); connect(this, SIGNAL(sqlHistoryRefreshNeeded()), this, SLOT(refreshSqlHistory())); connect(this, SIGNAL(ddlHistoryRefreshNeeded()), this, SLOT(refreshDdlHistory())); } void ConfigImpl::cleanUp() { if (db->isOpen()) db->close(); safe_delete(db) } const QString &ConfigImpl::getConfigDir() const { return configDir; } QString ConfigImpl::getConfigFilePath() const { if (!db) return QString(); return db->getPath(); } bool ConfigImpl::isInMemory() const { return db->getPath() == memoryDbName; } void ConfigImpl::beginMassSave() { if (isMassSaving()) return; emit massSaveBegins(); db->exec("BEGIN;"); massSaving = true; } void ConfigImpl::commitMassSave() { if (!isMassSaving()) return; db->exec("COMMIT;"); emit massSaveCommitted(); massSaving = false; } void ConfigImpl::rollbackMassSave() { if (!isMassSaving()) return; db->exec("ROLLBACK;"); massSaving = false; } bool ConfigImpl::isMassSaving() const { return massSaving; } void ConfigImpl::set(const QString &group, const QString &key, const QVariant &value) { db->exec("INSERT OR REPLACE INTO settings VALUES (?, ?, ?)", {group, key, serializeToBytes(value)}); } QVariant ConfigImpl::get(const QString &group, const QString &key) { SqlQueryPtr results = db->exec("SELECT value FROM settings WHERE [group] = ? AND [key] = ?", {group, key}); return deserializeValue(results->getSingleCell()); } QVariant ConfigImpl::get(const QString &group, const QString &key, const QVariant &defaultValue) { QVariant value = get(group, key); if (!value.isValid() || value.isNull()) return defaultValue; return value; } QHash ConfigImpl::getAll() { SqlQueryPtr results = db->exec("SELECT [group], [key], value FROM settings"); QHash cfg; QString key; SqlResultsRowPtr row; while (results->hasNext()) { row = results->next(); key = row->value("group").toString() + "." + row->value("key").toString(); cfg[key] = deserializeValue(row->value("value")); } return cfg; } bool ConfigImpl::storeErrorAndReturn(SqlQueryPtr results) { if (results->isError()) { lastQueryError = results->getErrorText(); return true; } else return false; } void ConfigImpl::printErrorIfSet(SqlQueryPtr results) { if (results && results->isError()) { qCritical() << "Config error while executing query:" << results->getErrorText(); storeErrorAndReturn(results); } } bool ConfigImpl::addDb(const QString& name, const QString& path, const QHash& options) { QByteArray optBytes = hashToBytes(options); SqlQueryPtr results = db->exec("INSERT INTO dblist VALUES (?, ?, ?)", {name, path, optBytes}); return !storeErrorAndReturn(results); } bool ConfigImpl::updateDb(const QString &name, const QString &newName, const QString &path, const QHash &options) { QByteArray optBytes = hashToBytes(options); SqlQueryPtr results = db->exec("UPDATE dblist SET name = ?, path = ?, options = ? WHERE name = ?", {newName, path, optBytes, name}); return (!storeErrorAndReturn(results) && results->rowsAffected() > 0); } bool ConfigImpl::removeDb(const QString &name) { SqlQueryPtr results = db->exec("DELETE FROM dblist WHERE name = ?", {name}); return (!storeErrorAndReturn(results) && results->rowsAffected() > 0); } bool ConfigImpl::isDbInConfig(const QString &name) { SqlQueryPtr results = db->exec("SELECT * FROM dblist WHERE name = ?", {name}); return (!storeErrorAndReturn(results) && results->hasNext()); } QString ConfigImpl::getLastErrorString() const { QString msg = db->getErrorText(); if (msg.trimmed().isEmpty()) return lastQueryError; return msg; } QString ConfigImpl::getSqlite3Version() const { return sqlite3Version; } QList ConfigImpl::dbList() { QList entries; SqlQueryPtr results = db->exec("SELECT name, path, options FROM dblist"); CfgDbPtr cfgDb; SqlResultsRowPtr row; while (results->hasNext()) { row = results->next(); cfgDb = CfgDbPtr::create(); cfgDb->name = row->value("name").toString(); cfgDb->path = row->value("path").toString(); cfgDb->options = deserializeValue(row->value("options")).toHash(); entries += cfgDb; } return entries; } ConfigImpl::CfgDbPtr ConfigImpl::getDb(const QString& dbName) { SqlQueryPtr results = db->exec("SELECT path, options FROM dblist WHERE name = ?", {dbName}); if (!results->hasNext()) return CfgDbPtr(); SqlResultsRowPtr row = results->next(); CfgDbPtr cfgDb = CfgDbPtr::create(); cfgDb->name = dbName; cfgDb->path = row->value("path").toString(); cfgDb->options = deserializeValue(row->value("options")).toHash(); return cfgDb; } void ConfigImpl::storeGroups(const QList& groups) { db->begin(); db->exec("DELETE FROM groups"); for (const DbGroupPtr& group : groups) storeGroup(group); db->commit(); } void ConfigImpl::storeGroup(const ConfigImpl::DbGroupPtr &group, qint64 parentId) { QVariant parent = QVariant(QVariant::LongLong); if (parentId > -1) parent = parentId; SqlQueryPtr results = db->exec("INSERT INTO groups (name, [order], parent, open, dbname, db_expanded) VALUES (?, ?, ?, ?, ?, ?)", {group->name, group->order, parent, group->open, group->referencedDbName, group->dbExpanded}); qint64 newParentId = results->getRegularInsertRowId(); for (const DbGroupPtr& childGroup : group->childs) storeGroup(childGroup, newParentId); } QList ConfigImpl::getGroups() { DbGroupPtr topGroup = DbGroupPtr::create(); topGroup->id = -1; readGroupRecursively(topGroup); return topGroup->childs; } ConfigImpl::DbGroupPtr ConfigImpl::getDbGroup(const QString& dbName) { SqlQueryPtr results = db->exec("SELECT id, name, [order], open, dbname, db_expanded FROM groups WHERE dbname = ? LIMIT 1", {dbName}); DbGroupPtr group = DbGroupPtr::create(); group->referencedDbName = dbName; if (!results->hasNext()) return group; SqlResultsRowPtr row = results->next(); group->id = row->value("id").toULongLong(); group->name = row->value("name").toString(); group->order = row->value("order").toInt(); group->open = row->value("open").toBool(); group->dbExpanded = row->value("db_expanded").toBool(); return group; } qint64 ConfigImpl::addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) { if (sqlHistoryId < 0) { SqlQueryPtr results = db->exec("SELECT max(id) FROM sqleditor_history"); if (results->isError()) { qCritical() << "Cannot add SQL history, because cannot determinate sqleditor_history max(id):" << results->getErrorText(); return -1; } if (results->hasNext()) sqlHistoryId = results->getSingleCell().toLongLong() + 1; else sqlHistoryId = 0; } sqlHistoryMutex.lock(); QtConcurrent::run(this, &ConfigImpl::asyncAddSqlHistory, sqlHistoryId, sql, dbName, timeSpentMillis, rowsAffected); return sqlHistoryId++; } void ConfigImpl::updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) { sqlHistoryMutex.lock(); QtConcurrent::run(this, &ConfigImpl::asyncUpdateSqlHistory, id, sql, dbName, timeSpentMillis, rowsAffected); } void ConfigImpl::clearSqlHistory() { QtConcurrent::run(this, &ConfigImpl::asyncClearSqlHistory); } void ConfigImpl::deleteSqlHistory(const QList& ids) { QtConcurrent::run(this, &ConfigImpl::asyncDeleteSqlHistory, ids); } QAbstractItemModel* ConfigImpl::getSqlHistoryModel() { if (!sqlHistoryModel) sqlHistoryModel = new SqlHistoryModel(db, this); return sqlHistoryModel; } void ConfigImpl::addCliHistory(const QString& text) { QtConcurrent::run(this, &ConfigImpl::asyncAddCliHistory, text); } void ConfigImpl::applyCliHistoryLimit() { QtConcurrent::run(this, &ConfigImpl::asyncApplyCliHistoryLimit); } void ConfigImpl::clearCliHistory() { QtConcurrent::run(this, &ConfigImpl::asyncClearCliHistory); } QStringList ConfigImpl::getCliHistory() const { static_qstring(selectQuery, "SELECT text FROM cli_history ORDER BY id"); SqlQueryPtr results = db->exec(selectQuery); if (results->isError()) qWarning() << "Error while getting CLI history:" << db->getErrorText(); return results->columnAsList("text"); } void ConfigImpl::addBindParamHistory(const QVector >& params) { QtConcurrent::run(this, &ConfigImpl::asyncAddBindParamHistory, params); } void ConfigImpl::applyBindParamHistoryLimit() { QtConcurrent::run(this, &ConfigImpl::asyncApplyBindParamHistoryLimit); } QVector> ConfigImpl::getBindParamHistory(const QStringList& paramNames) const { static_qstring(directQuery, "SELECT id FROM bind_params WHERE pattern = ? ORDER BY id DESC"); static_qstring(paramsByIdQuery, "SELECT name, value FROM bind_param_values WHERE bind_params_id = ? ORDER BY position"); static_qstring(singleParamQuery, "SELECT value FROM bind_param_values WHERE %1 = ? ORDER BY id DESC LIMIT 1;"); static_qstring(singleParamName, "name"); static_qstring(singleParamPosition, "position"); QVector> bindParams; bindParams.reserve(paramNames.size()); SqlQueryPtr results = db->exec(directQuery, {paramNames.join(",")}); if (results->isError()) { qWarning() << "Error while getting BindParams (1):" << db->getErrorText(); return bindParams; } // Got an exact match? Extract values and return. QVariant exactMatch = results->getSingleCell(); if (!exactMatch.isNull()) { results = db->exec(paramsByIdQuery, {exactMatch.toLongLong()}); if (results->isError()) { qWarning() << "Error while getting BindParams (2):" << db->getErrorText(); } else { for (const SqlResultsRowPtr& row : results->getAll()) bindParams << QPair(row->value("name").toString(), row->value("value")); } return bindParams; } // No exact match. Will look for values one by one using param name and position. int position = 0; for (const QString& bindParam : paramNames) { if (bindParam == "?") results = db->exec(singleParamQuery.arg(singleParamPosition), {position}); else results = db->exec(singleParamQuery.arg(singleParamName), {bindParam}); bindParams << QPair(bindParam, results->getSingleCell()); position++; } return bindParams; } void ConfigImpl::addPopulateHistory(const QString& database, const QString& table, int rows, const QHash >& columnsPluginsConfig) { QtConcurrent::run(this, &ConfigImpl::asyncAddPopulateHistory, database, table, rows, columnsPluginsConfig); } void ConfigImpl::applyPopulateHistoryLimit() { QtConcurrent::run(this, &ConfigImpl::asyncApplyPopulateHistoryLimit); } QHash> ConfigImpl::getPopulateHistory(const QString& database, const QString& table, int& rows) const { static_qstring(initialQuery, "SELECT id, rows FROM populate_history WHERE [database] = ? AND [table] = ? ORDER BY id DESC LIMIT 1"); static_qstring(columnsQuery, "SELECT column_name, plugin_name, plugin_config FROM populate_column_history WHERE populate_history_id = ?"); QHash> historyEntry; SqlQueryPtr results = db->exec(initialQuery, {database, table}); if (results->isError()) { qWarning() << "Error while getting Populating history entry (1):" << db->getErrorText(); return historyEntry; } if (!results->hasNext()) return historyEntry; SqlResultsRowPtr row = results->next(); qint64 historyEntryId = row->value("id").toLongLong(); rows = row->value("rows").toInt(); results = db->exec(columnsQuery, {historyEntryId}); QVariant value; while (results->hasNext()) { row = results->next(); value = deserializeValue(row->value("plugin_config")); historyEntry[row->value("column_name").toString()] = QPair(row->value("plugin_name").toString(), value); } return historyEntry; } QVariant ConfigImpl::getPopulateHistory(const QString& pluginName) const { static_qstring(columnsQuery, "SELECT plugin_config FROM populate_column_history WHERE plugin_name = ? ORDER BY id DESC LiMIT 1"); SqlQueryPtr results = db->exec(columnsQuery, {pluginName}); if (results->isError()) { qWarning() << "Error while getting Populating history entry (2):" << db->getErrorText(); return QVariant(); } return deserializeValue(results->getSingleCell()); } void ConfigImpl::addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile) { ddlHistoryMutex.lock(); QtConcurrent::run(this, &ConfigImpl::asyncAddDdlHistory, queries, dbName, dbFile); } QList ConfigImpl::getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date) { static_qstring(sql, "SELECT timestamp," " queries" " FROM ddl_history" " WHERE dbname = ?" " AND file = ?" " AND date(timestamp, 'unixepoch') = ?"); SqlQueryPtr results = db->exec(sql, {dbName, dbFile, date.toString("yyyy-MM-dd")}); QList entries; DdlHistoryEntryPtr entry; SqlResultsRowPtr row; while (results->hasNext()) { row = results->next(); entry = DdlHistoryEntryPtr::create(); entry->dbName = dbName; entry->dbFile = dbFile; entry->timestamp = QDateTime::fromTime_t(row->value("timestamp").toUInt()); entry->queries = row->value("queries").toString(); entries << entry; } return entries; } DdlHistoryModel* ConfigImpl::getDdlHistoryModel() { if (!ddlHistoryModel) ddlHistoryModel = new DdlHistoryModel(db, this); return ddlHistoryModel; } void ConfigImpl::clearDdlHistory() { QtConcurrent::run(this, &ConfigImpl::asyncClearDdlHistory); } void ConfigImpl::addReportHistory(bool isFeatureRequest, const QString& title, const QString& url) { QtConcurrent::run(this, &ConfigImpl::asyncAddReportHistory, isFeatureRequest, title, url); } QList ConfigImpl::getReportHistory() { static_qstring(sql, "SELECT id, timestamp, title, url, feature_request FROM reports_history"); SqlQueryPtr results = db->exec(sql); QList entries; SqlResultsRowPtr row; ReportHistoryEntryPtr entry; while (results->hasNext()) { row = results->next(); entry = ReportHistoryEntryPtr::create(); entry->id = row->value("id").toInt(); entry->timestamp = row->value("timestamp").toInt(); entry->title = row->value("title").toString(); entry->url = row->value("url").toString(); entry->isFeatureRequest = row->value("feature_request").toBool(); entries << entry; } return entries; } void ConfigImpl::deleteReport(int id) { QtConcurrent::run(this, &ConfigImpl::asyncDeleteReport, id); } void ConfigImpl::clearReportHistory() { QtConcurrent::run(this, &ConfigImpl::asyncClearReportHistory); } void ConfigImpl::readGroupRecursively(ConfigImpl::DbGroupPtr group) { SqlQueryPtr results; if (group->id < 0) results = db->exec("SELECT id, name, [order], open, dbname, db_expanded FROM groups WHERE parent IS NULL ORDER BY [order]"); else results = db->exec("SELECT id, name, [order], open, dbname, db_expanded FROM groups WHERE parent = ? ORDER BY [order]", {group->id}); DbGroupPtr childGroup; SqlResultsRowPtr row; while (results->hasNext()) { row = results->next(); childGroup = DbGroupPtr::create(); childGroup->id = row->value("id").toULongLong(); childGroup->name = row->value("name").toString(); childGroup->order = row->value("order").toInt(); childGroup->open = row->value("open").toBool(); childGroup->referencedDbName = row->value("dbname").toString(); childGroup->dbExpanded = row->value("db_expanded").toBool(); group->childs += childGroup; } for (int i = 0; i < group->childs.size(); i++) readGroupRecursively(group->childs[i]); } void ConfigImpl::begin() { db->begin(); } void ConfigImpl::commit() { db->commit(); } void ConfigImpl::rollback() { db->rollback(); } QString ConfigImpl::getConfigPath() { return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/" + DB_FILE_NAME; } QString ConfigImpl::getLegacyConfigPath() { #ifdef Q_OS_WIN return SQLITESTUDIO->getEnv("APPDATA")+"/sqlitestudio"; #else return SQLITESTUDIO->getEnv("HOME")+"/.config/sqlitestudio"; #endif } void ConfigImpl::initTables() { SqlQueryPtr results = db->exec("SELECT lower(name) AS name FROM sqlite_master WHERE type = 'table'"); QList tables = results->columnAsList(0); if (!tables.contains("version")) { for (const QString& table : tables) db->exec("DROP TABLE "+table); tables.clear(); db->exec("CREATE TABLE version (version NUMERIC)"); db->exec("INSERT INTO version VALUES ("+QString::number(SQLITESTUDIO_CONFIG_VERSION)+")"); } if (!tables.contains("settings")) db->exec("CREATE TABLE settings ([group] TEXT, [key] TEXT, value, PRIMARY KEY([group], [key]))"); if (!tables.contains("sqleditor_history")) db->exec("CREATE TABLE sqleditor_history (id INTEGER PRIMARY KEY, dbname TEXT, date INTEGER, time_spent INTEGER, rows INTEGER, sql TEXT)"); if (!tables.contains("dblist")) db->exec("CREATE TABLE dblist (name TEXT PRIMARY KEY, path TEXT UNIQUE, options TEXT)"); if (!tables.contains("groups")) db->exec("CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, parent INTEGER REFERENCES groups(id), " "[order] INTEGER, open INTEGER DEFAULT 0, dbname TEXT UNIQUE REFERENCES dblist(name) ON UPDATE CASCADE ON DELETE CASCADE, " "db_expanded INTEGER DEFAULT 0, UNIQUE(name, parent))"); if (!tables.contains("ddl_history")) db->exec("CREATE TABLE ddl_history (id INTEGER PRIMARY KEY AUTOINCREMENT, dbname TEXT, file TEXT, timestamp INTEGER, " "queries TEXT)"); if (!tables.contains("cli_history")) db->exec("CREATE TABLE cli_history (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)"); if (!tables.contains("reports_history")) db->exec("CREATE TABLE reports_history (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, feature_request BOOLEAN, title TEXT, url TEXT)"); if (!tables.contains("bind_params")) { db->exec("CREATE TABLE bind_params (id INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT NOT NULL)"); db->exec("CREATE INDEX bind_params_patt_idx ON bind_params (pattern);"); } if (!tables.contains("bind_param_values")) { db->exec("CREATE TABLE bind_param_values (id INTEGER PRIMARY KEY AUTOINCREMENT, bind_params_id INTEGER REFERENCES bind_params (id) " "ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, position INTEGER NOT NULL, name TEXT NOT NULL, value)"); db->exec("CREATE INDEX bind_param_values_fk_idx ON bind_param_values (bind_params_id);"); } if (!tables.contains("populate_history")) { db->exec("CREATE TABLE populate_history (id INTEGER PRIMARY KEY AUTOINCREMENT, [database] TEXT NOT NULL, [table] TEXT NOT NULL, rows INTEGER NOT NULL)"); } if (!tables.contains("populate_column_history")) { db->exec("CREATE TABLE populate_column_history (id INTEGER PRIMARY KEY AUTOINCREMENT, populate_history_id INTEGER REFERENCES populate_history (id) " "ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, column_name TEXT NOT NULL, plugin_name TEXT NOT NULL, plugin_config BLOB)"); db->exec("CREATE INDEX populate_plugin_history_idx ON populate_column_history (plugin_name)"); } if (!tables.contains("reports_history")) db->exec("CREATE TABLE reports_history (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, feature_request BOOLEAN, title TEXT, url TEXT)"); } void ConfigImpl::initDbFile() { QList paths = getStdDbPaths(); // A fallback to in-memory db paths << ConfigDirCandidate{memoryDbName, false, false}; // Go through all candidates and pick one QDir dir; for (ConfigDirCandidate& path : paths) { dir = QDir(path.path); if (path.path != memoryDbName) dir = QFileInfo(path.path).dir(); if (tryInitDbFile(path)) { configDir = dir.absolutePath(); break; } } // If not one of std paths, look for the one from previously saved. if (configDir == memoryDbName) { // Do we have path selected by user? (because app was unable to find writable location before) QSettings* sett = getSettings(); QString path = sett->value(CONFIG_DIR_SETTING).toString(); if (!path.isEmpty()) { paths << ConfigDirCandidate{path + "/" + DB_FILE_NAME, false, false}; qDebug() << "Using custom configuration directory. The location is stored in" << sett->fileName(); } } // Failed to use any of predefined directories. Let's ask the user. while (configDir == memoryDbName) { QString path = askUserForConfigDirFunc(); if (path.isNull()) break; dir = QDir(path); if (tryInitDbFile(ConfigDirCandidate{path + "/" + DB_FILE_NAME, false, false})) { configDir = dir.absolutePath(); QSettings* sett = getSettings(); sett->setValue(CONFIG_DIR_SETTING, configDir); qDebug() << "Using custom configuration directory. The location is stored in" << sett->fileName(); } } // We ended up with in-memory one? That's not good. if (configDir == memoryDbName) { paths.removeLast(); QStringList pathStrings; for (ConfigDirCandidate& path : paths) pathStrings << path.path; notifyError(QObject::tr("Could not initialize configuration file. Any configuration changes and queries history will be lost after application restart." " Unable to create a file at following locations: %1.").arg(pathStrings.join(", "))); } qDebug().noquote() << "Using configuration directory:" << toNativePath(configDir); db->exec("PRAGMA foreign_keys = 1;"); } QList ConfigImpl::getStdDbPaths() { QList paths; // Portable dir location has always precedense - comes first QString portablePath = getPortableConfigPath(); if (!portablePath.isNull()) paths << ConfigDirCandidate{portablePath+"/"+DB_FILE_NAME, false, true}; // Determinate global config location QString globalPath = getConfigPath(); paths << ConfigDirCandidate{globalPath, true, false}; // If needed, migrate configuration from legacy location (pre-3.3) to new location (3.3 and later) QString legacyGlobalPath = getLegacyConfigPath(); if (!legacyGlobalPath.isNull()) { paths << ConfigDirCandidate{legacyGlobalPath+"/"+DB_FILE_NAME, true, false}; if (!QFile::exists(globalPath)) tryToMigrateOldGlobalPath(legacyGlobalPath, globalPath); } return paths; } bool ConfigImpl::tryInitDbFile(const ConfigDirCandidate& dbPath) { // Create global config directory if not existing if (dbPath.createIfNotExists && !dbPath.path.isNull()) { QDir dir(dbPath.path.mid(0, dbPath.path.length() - DB_FILE_NAME.length() - 1)); if (!dir.exists()) QDir::root().mkpath(dir.absolutePath()); } db = new DbSqlite3("SQLiteStudio settings", dbPath.path, {{DB_PURE_INIT, true}}); if (!db->open()) { safe_delete(db); return false; } SqlQueryPtr results = db->exec("SELECT * FROM sqlite_master"); if (results->isError()) { safe_delete(db); return false; } return true; } QVariant ConfigImpl::deserializeValue(const QVariant &value) const { if (!value.isValid()) return QVariant(); QByteArray bytes = value.toByteArray(); return deserializeFromBytes(bytes); } void ConfigImpl::asyncAddSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) { db->begin(); SqlQueryPtr results = db->exec("INSERT INTO sqleditor_history (id, dbname, date, time_spent, rows, sql) VALUES (?, ?, ?, ?, ?, ?)", {id, dbName, (QDateTime::currentMSecsSinceEpoch() / 1000), timeSpentMillis, rowsAffected, sql}); if (results->isError()) { qDebug() << "Error adding SQL history:" << results->getErrorText(); db->rollback(); sqlHistoryMutex.unlock(); return; } int maxHistorySize = CFG_CORE.General.SqlHistorySize.get(); results = db->exec("SELECT count(*) FROM sqleditor_history"); if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize) { results = db->exec(QString("SELECT id FROM sqleditor_history ORDER BY id DESC LIMIT 1 OFFSET %1").arg(maxHistorySize)); if (results->hasNext()) { int id = results->getSingleCell().toInt(); if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever. db->exec("DELETE FROM sqleditor_history WHERE id <= ?", {id}); } } db->commit(); emit sqlHistoryRefreshNeeded(); sqlHistoryMutex.unlock(); } void ConfigImpl::asyncUpdateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) { db->exec("UPDATE sqleditor_history SET dbname = ?, time_spent = ?, rows = ?, sql = ? WHERE id = ?", {dbName, timeSpentMillis, rowsAffected, sql, id}); emit sqlHistoryRefreshNeeded(); sqlHistoryMutex.unlock(); } void ConfigImpl::asyncClearSqlHistory() { db->exec("DELETE FROM sqleditor_history"); emit sqlHistoryRefreshNeeded(); } void ConfigImpl::asyncDeleteSqlHistory(const QList& ids) { if (!db->begin()) { NOTIFY_MANAGER->warn(tr("Could not start database transaction for deleting SQL history, therefore it's not deleted.")); return; } for (const qint64& id : ids) db->exec("DELETE FROM sqleditor_history WHERE id = ?", id); if (!db->commit()) { NOTIFY_MANAGER->warn(tr("Could not commit database transaction for deleting SQL history, therefore it's not deleted.")); db->rollback(); return; } emit sqlHistoryRefreshNeeded(); } void ConfigImpl::asyncAddCliHistory(const QString& text) { static_qstring(insertQuery, "INSERT INTO cli_history (text) VALUES (?)"); SqlQueryPtr results = db->exec(insertQuery, {text}); if (results->isError()) qWarning() << "Error while adding CLI history:" << results->getErrorText(); applyCliHistoryLimit(); } void ConfigImpl::asyncApplyCliHistoryLimit() { static_qstring(limitQuery, "DELETE FROM cli_history WHERE id <= (SELECT id FROM cli_history ORDER BY id DESC LIMIT 1 OFFSET %1)"); SqlQueryPtr results = db->exec(limitQuery.arg(CFG_CORE.Console.HistorySize.get())); if (results->isError()) qWarning() << "Error while limiting CLI history:" << db->getErrorText(); } void ConfigImpl::asyncClearCliHistory() { static_qstring(clearQuery, "DELETE FROM cli_history"); SqlQueryPtr results = db->exec(clearQuery); if (results->isError()) qWarning() << "Error while clearing CLI history:" << db->getErrorText(); } void ConfigImpl::asyncAddBindParamHistory(const QVector >& params) { static_qstring(insertParamsQuery, "INSERT INTO bind_params (pattern) VALUES (?)"); static_qstring(insertValuesQuery, "INSERT INTO bind_param_values (bind_params_id, position, name, value) VALUES (?, ?, ?, ?)"); if (!db->begin()) { qWarning() << "Failed to store BindParam cache, because could not begin SQL transaction. Details:" << db->getErrorText(); return; } QStringList paramNames; for (const QPair& paramPair : params) paramNames << paramPair.first; SqlQueryPtr results = db->exec(insertParamsQuery, {paramNames.join(",")}); RowId rowId = results->getInsertRowId(); qint64 bindParamsId = rowId["ROWID"].toLongLong(); int position = 0; for (const QPair& paramPair : params) { results = db->exec(insertValuesQuery, {bindParamsId, position++, paramPair.first, paramPair.second}); if (results->isError()) { qWarning() << "Failed to store BindParam cache, due to SQL error:" << db->getErrorText(); db->rollback(); return; } } if (!db->commit()) { qWarning() << "Failed to store BindParam cache, because could not commit SQL transaction. Details:" << db->getErrorText(); db->rollback(); } asyncApplyBindParamHistoryLimit(); } void ConfigImpl::asyncApplyBindParamHistoryLimit() { static_qstring(findBindParamIdQuery, "SELECT bind_params_id FROM bind_param_values ORDER BY id DESC LIMIT 1 OFFSET %1"); static_qstring(limitBindParamsQuery, "DELETE FROM bind_params WHERE id <= ?"); // will cascade with FK to bind_param_values SqlQueryPtr results = db->exec(findBindParamIdQuery.arg(CFG_CORE.General.BindParamsCacheSize.get())); if (results->isError()) qWarning() << "Error while limiting BindParam history (step 1):" << db->getErrorText(); qint64 bindParamId = results->getSingleCell().toLongLong(); results = db->exec(limitBindParamsQuery, {bindParamId}); if (results->isError()) qWarning() << "Error while limiting BindParam history (step 2):" << db->getErrorText(); } void ConfigImpl::asyncAddPopulateHistory(const QString& database, const QString& table, int rows, const QHash>& columnsPluginsConfig) { static_qstring(insertQuery, "INSERT INTO populate_history ([database], [table], rows) VALUES (?, ?, ?)"); static_qstring(insertColumnQuery, "INSERT INTO populate_column_history (populate_history_id, column_name, plugin_name, plugin_config) VALUES (?, ?, ?, ?)"); if (!db->begin()) { qWarning() << "Failed to store Populating history entry, because could not begin SQL transaction. Details:" << db->getErrorText(); return; } SqlQueryPtr results = db->exec(insertQuery, {database, table, rows}); RowId rowId = results->getInsertRowId(); qint64 populateHistoryId = rowId["ROWID"].toLongLong(); for (QHash>::const_iterator colIt = columnsPluginsConfig.begin(); colIt != columnsPluginsConfig.end(); colIt++) { results = db->exec(insertColumnQuery, {populateHistoryId, colIt.key(), colIt.value().first, serializeToBytes(colIt.value().second)}); if (results->isError()) { qWarning() << "Failed to store Populating history entry, due to SQL error:" << db->getErrorText(); db->rollback(); return; } } if (!db->commit()) { qWarning() << "Failed to store Populating history entry, because could not commit SQL transaction. Details:" << db->getErrorText(); db->rollback(); } asyncApplyPopulateHistoryLimit(); } void ConfigImpl::asyncApplyPopulateHistoryLimit() { static_qstring(limitQuery, "DELETE FROM populate_history WHERE id <= (SELECT id FROM populate_history ORDER BY id DESC LIMIT 1 OFFSET %1)"); SqlQueryPtr results = db->exec(limitQuery.arg(CFG_CORE.General.PopulateHistorySize.get())); if (results->isError()) qWarning() << "Error while limiting Populating history:" << db->getErrorText(); } void ConfigImpl::asyncAddDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile) { static_qstring(insert, "INSERT INTO ddl_history (dbname, file, timestamp, queries) VALUES (?, ?, ?, ?)"); static_qstring(countSql, "SELECT count(*) FROM ddl_history"); static_qstring(idSql, "SELECT id FROM ddl_history ORDER BY id DESC LIMIT 1 OFFSET %1"); static_qstring(deleteSql, "DELETE FROM ddl_history WHERE id <= ?"); db->begin(); db->exec(insert, {dbName, dbFile, QDateTime::currentDateTime().toTime_t(), queries}); int maxHistorySize = CFG_CORE.General.DdlHistorySize.get(); SqlQueryPtr results = db->exec(countSql); if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize) { results = db->exec(QString(idSql).arg(maxHistorySize), Db::Flag::NO_LOCK); if (results->hasNext()) { int id = results->getSingleCell().toInt(); if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever. db->exec(deleteSql, {id}); } } db->commit(); ddlHistoryMutex.unlock(); emit ddlHistoryRefreshNeeded(); } void ConfigImpl::asyncClearDdlHistory() { db->exec("DELETE FROM ddl_history"); emit ddlHistoryRefreshNeeded(); } void ConfigImpl::asyncAddReportHistory(bool isFeatureRequest, const QString& title, const QString& url) { static_qstring(sql, "INSERT INTO reports_history (feature_request, timestamp, title, url) VALUES (?, ?, ?, ?)"); db->exec(sql, {(isFeatureRequest ? 1 : 0), QDateTime::currentDateTime().toTime_t(), title, url}); emit reportsHistoryRefreshNeeded(); } void ConfigImpl::asyncDeleteReport(int id) { static_qstring(sql, "DELETE FROM reports_history WHERE id = ?"); db->exec(sql, {id}); emit reportsHistoryRefreshNeeded(); } void ConfigImpl::asyncClearReportHistory() { static_qstring(sql, "DELETE FROM reports_history"); db->exec(sql); emit reportsHistoryRefreshNeeded(); } void ConfigImpl::mergeMasterConfig() { QString masterConfigFile = Config::getMasterConfigFile(); if (masterConfigFile.isEmpty()) return; #if QT_VERSION >= 0x050500 qInfo() #else qDebug() #endif << "Updating settings from master configuration file: " << masterConfigFile; Db* masterDb = new DbSqlite3("SQLiteStudio master settings", masterConfigFile, {{DB_PURE_INIT, true}}); if (!masterDb->open()) { safe_delete(masterDb); qWarning() << "Could not open master config database:" << masterConfigFile; return; } SqlQueryPtr results = masterDb->exec("SELECT [group], key, value FROM settings"); if (results->isError()) { qWarning() << "Could not query master config database:" << masterConfigFile << ", error details:" << results->getErrorText(); safe_delete(masterDb); return; } static_qstring(insertSql, "INSERT OR IGNORE INTO settings ([group], key, value) VALUES (?, ?, ?)"); db->begin(); SqlResultsRowPtr row; while (results->hasNext()) { row = results->next(); if (row->value("group") == "General" && row->value("key") == "Session") continue; // Don't copy session db->exec(insertSql, row->valueList()); } db->commit(); masterDb->close(); safe_delete(masterDb); } void ConfigImpl::updateConfigDb() { SqlQueryPtr result = db->exec("SELECT version FROM version LIMIT 1"); int dbVersion = result->getSingleCell().toInt(); if (dbVersion >= SQLITESTUDIO_CONFIG_VERSION) return; db->begin(); switch (dbVersion) { case 1: { // 1->2 db->exec("UPDATE settings SET [key] = 'DataUncommittedError' WHERE [key] = 'DataUncommitedError'"); db->exec("UPDATE settings SET [key] = 'DataUncommitted' WHERE [key] = 'DataUncommited'"); [[fallthrough]]; } case 2: { // 2->3 db->exec("ALTER TABLE groups ADD db_expanded INTEGER DEFAULT 0"); [[fallthrough]]; } case 3: { // 3->4 db->exec("DELETE FROM settings WHERE [group] = 'DialogDimensions'"); // #5161 //[[fallthrough]]; } // Add cases here for next versions, // without a "break" instruction, // in order to update from certain // version to latest at once. } db->exec("UPDATE version SET version = ?", {SQLITESTUDIO_CONFIG_VERSION}); db->commit(); } bool ConfigImpl::tryToMigrateOldGlobalPath(const QString& oldPath, const QString& newPath) { if (!QFileInfo::exists(oldPath)) return false; qDebug().noquote() << "Attempting to migrate legacy config location" << toNativePath(oldPath) << "to new location" << toNativePath(newPath); QDir dir = QFileInfo(newPath).dir(); if (!dir.exists()) QDir::root().mkpath(dir.absolutePath()); if (QFile::copy(oldPath, dir.absoluteFilePath(DB_FILE_NAME))) { qDebug() << "Migration successful. Renaming old location file so it has '.old' suffix."; if (QFile::rename(oldPath, oldPath+".old")) qDebug() << "Renaming successful."; else qDebug() << "Renaming did not work, but it's okay. It will just remain with original name there."; } else qDebug() << "Migration (copying) failed."; return true; } void ConfigImpl::refreshSqlHistory() { if (sqlHistoryModel) sqlHistoryModel->refresh(); } void ConfigImpl::refreshDdlHistory() { if (ddlHistoryModel) ddlHistoryModel->refresh(); }