diff --git a/distri/sqlitebrowser.desktop.appdata.xml b/distri/sqlitebrowser.desktop.appdata.xml index a231014e4..86861ddaf 100644 --- a/distri/sqlitebrowser.desktop.appdata.xml +++ b/distri/sqlitebrowser.desktop.appdata.xml @@ -46,6 +46,11 @@ https://sqlitebrowser.org/ https://github.com/sqlitebrowser/sqlitebrowser/issues + https://www.patreon.com/db4s + https://github.com/sqlitebrowser/sqlitebrowser/wiki/Frequently-Asked-Questions + https://github.com/sqlitebrowser/sqlitebrowser/wiki/Translations + https://github.com/sqlitebrowser/sqlitebrowser/wiki#for-developers + https://github.com/sqlitebrowser/sqlitebrowser diff --git a/src/AddRecordDialog.cpp b/src/AddRecordDialog.cpp index 2aa3e1e4b..0954389b5 100644 --- a/src/AddRecordDialog.cpp +++ b/src/AddRecordDialog.cpp @@ -190,6 +190,8 @@ void AddRecordDialog::populateFields() // Initialize fields, fks and pk differently depending on whether it's a table or a view. const sqlb::TablePtr table = pdb.getTableByName(curTable); + if(!table) + return; fields = table->fields; if (!table->isView()) { diff --git a/src/EditTableDialog.cpp b/src/EditTableDialog.cpp index 54ee0619f..f4b452bbb 100644 --- a/src/EditTableDialog.cpp +++ b/src/EditTableDialog.cpp @@ -81,7 +81,10 @@ EditTableDialog::EditTableDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier& if(m_bNewTable == false) { // Existing table, so load and set the current layout - m_table = *pdb.getTableByName(curTable); + auto tbl = pdb.getTableByName(curTable); + if(!tbl) + return; + m_table = *tbl; ui->labelEditWarning->setVisible(!m_table.fullyParsed()); // Initialise the list of tracked columns for table layout changes @@ -661,8 +664,11 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column) // we need to check for this case and cancel here. Maybe we can think of some way to modify the INSERT INTO ... SELECT statement // to at least replace all troublesome NULL values by the default value SqliteTableModel m(pdb, this); + auto tbl = pdb.getTableByName(curTable); + if(!tbl) + return; m.setQuery(QString("SELECT COUNT(%1) FROM %2 WHERE coalesce(NULL,%3) IS NULL;").arg( - QString::fromStdString(sqlb::joinStringVector(sqlb::escapeIdentifier(pdb.getTableByName(curTable)->rowidColumns()), ",")), + QString::fromStdString(sqlb::joinStringVector(sqlb::escapeIdentifier(tbl->rowidColumns()), ",")), QString::fromStdString(curTable.toString()), QString::fromStdString(sqlb::escapeIdentifier(field.name())))); if(!m.completeCache()) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index 1b8dd5b17..efcdb0256 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -147,11 +147,15 @@ QWidget* ExtendedTableWidgetEditorDelegate::createEditor(QWidget* parent, const // If no column name is set, assume the primary key is meant if(fk->columns().empty()) { sqlb::TablePtr obj = m->db().getTableByName(foreignTable); + if(!obj) + return nullptr; column = obj->primaryKeyColumns().front().name(); } else column = fk->columns().at(0); sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName()); + if(!currentTable) + return nullptr; QString query = QString("SELECT %1 FROM %2").arg(QString::fromStdString(sqlb::escapeIdentifier(column)), QString::fromStdString(foreignTable.toString())); // if the current column of the current table does NOT have not-null constraint, @@ -257,6 +261,8 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : m_frozen_column_count(0), m_item_border_delegate(new ItemBorderDelegate(this)) { + setWordWrap(Settings::getValue("databrowser", "cell_word_wrap").toBool()); + setHorizontalScrollMode(ExtendedTableWidget::ScrollPerPixel); // Force ScrollPerItem, so scrolling shows all table rows setVerticalScrollMode(ExtendedTableWidget::ScrollPerItem); @@ -511,6 +517,8 @@ void ExtendedTableWidget::reloadSettings() verticalHeader()->setDefaultSectionSize(fontMetrics.height()+10); if(m_frozen_table_view) m_frozen_table_view->reloadSettings(); + + setWordWrap(Settings::getValue("databrowser", "cell_word_wrap").toBool()); } bool ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL) diff --git a/src/ImportCsvDialog.cpp b/src/ImportCsvDialog.cpp index 9921bb9f1..a982ee660 100644 --- a/src/ImportCsvDialog.cpp +++ b/src/ImportCsvDialog.cpp @@ -17,6 +17,7 @@ #include #include #include +#include // Enable this line to show basic performance stats after each imported CSV file. Please keep in mind that while these // numbers might help to estimate the performance of the algorithm, this is not a proper benchmark. @@ -71,11 +72,13 @@ ImportCsvDialog::ImportCsvDialog(const std::vector& filenames, DBBrowse ui->comboSeparator->blockSignals(true); ui->comboQuote->blockSignals(true); ui->comboEncoding->blockSignals(true); + ui->checkReplaceNonAlnum->blockSignals(true); ui->checkboxHeader->setChecked(Settings::getValue("importcsv", "firstrowheader").toBool()); ui->checkBoxTrimFields->setChecked(Settings::getValue("importcsv", "trimfields").toBool()); ui->checkBoxSeparateTables->setChecked(Settings::getValue("importcsv", "separatetables").toBool()); ui->checkLocalConventions->setChecked(Settings::getValue("importcsv", "localconventions").toBool()); + ui->checkReplaceNonAlnum->setChecked(Settings::getValue("importcsv", "replacenonalnum").toBool()); setSeparatorChar(getSettingsChar("importcsv", "separator")); setQuoteChar(getSettingsChar("importcsv", "quotecharacter")); setEncoding(Settings::getValue("importcsv", "encoding").toString()); @@ -87,6 +90,7 @@ ImportCsvDialog::ImportCsvDialog(const std::vector& filenames, DBBrowse ui->comboSeparator->blockSignals(false); ui->comboQuote->blockSignals(false); ui->comboEncoding->blockSignals(false); + ui->checkReplaceNonAlnum->blockSignals(false); // Prepare and show interface depending on how many files are selected if (csvFilenames.size() > 1) @@ -193,6 +197,7 @@ void ImportCsvDialog::accept() Settings::setValue("importcsv", "separatetables", ui->checkBoxSeparateTables->isChecked()); Settings::setValue("importcsv", "localconventions", ui->checkLocalConventions->isChecked()); Settings::setValue("importcsv", "encoding", currentEncoding()); + Settings::setValue("importcsv", "replacenonalnum", ui->checkReplaceNonAlnum->isChecked()); // Get all the selected files and start the import if (ui->filePickerBlock->isVisible()) @@ -425,6 +430,13 @@ sqlb::FieldVector ImportCsvDialog::generateFieldList(const QString& filename) co { // Take field name from CSV fieldname = std::string(rowData.fields[i].data, rowData.fields[i].data_length); + + // Replace any non-nlphanumeric characters with an underscore + if(ui->checkReplaceNonAlnum->isChecked()) + { + std::regex pattern("[^a-zA-Z0-9_]"); + fieldname = std::regex_replace(fieldname, pattern, "_"); + } } // If we don't have a field name by now, generate one @@ -879,6 +891,8 @@ void ImportCsvDialog::toggleAdvancedSection(bool show) ui->checkIgnoreDefaults->setVisible(show); ui->labelOnConflictStrategy->setVisible(show); ui->comboOnConflictStrategy->setVisible(show); + ui->labelReplaceNonAlnum->setVisible(show); + ui->checkReplaceNonAlnum->setVisible(show); } char32_t ImportCsvDialog::toUtf8(const QString& s) const diff --git a/src/ImportCsvDialog.ui b/src/ImportCsvDialog.ui index a711aa2a1..6083bc271 100644 --- a/src/ImportCsvDialog.ui +++ b/src/ImportCsvDialog.ui @@ -399,6 +399,23 @@ + + + + Replace non-alphanumeric characters in column name with an underscore. + + + + + + + Replace non-alphanumeric in column name + + + checkReplaceNonAlnum + + + @@ -793,6 +810,12 @@ + + checkReplaceNonAlnum + toggled(bool) + ImportCsvDialog + updatePreview() + updatePreview() diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 02335ca91..0cf9d38c7 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -583,7 +583,11 @@ bool MainWindow::fileOpen(const QString& fileName, bool openFromProject, bool re loadPragmas(); refreshTableBrowsers(); - + if (db.readOnly()) { + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); + fileSystemWatch.addPath(wFile); + } retval = true; } else { QMessageBox::warning(this, qApp->applicationName(), tr("Could not open database file.\nReason: %1").arg(db.lastError())); @@ -700,6 +704,10 @@ void MainWindow::refreshTableBrowsers(bool all) QApplication::restoreOverrideCursor(); } +void MainWindow::refreshDb() { + refreshTableBrowsers(); +} + bool MainWindow::fileSaveAs() { QString fileName = FileDialog::getSaveFileName( @@ -743,6 +751,8 @@ bool MainWindow::fileClose() if(!db.close()) return false; + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); TableBrowser::resetSharedSettings(); setCurrentFile(QString()); loadPragmas(); @@ -1032,6 +1042,8 @@ void MainWindow::editObject() refreshTableBrowsers(); } else if(type == "view") { sqlb::TablePtr view = db.getTableByName(obj); + if(!view) + return; runSqlNewTab(QString("DROP VIEW IF EXISTS %1;\n%2").arg(QString::fromStdString(obj.toString()), QString::fromStdString(view->sql())), tr("Edit View %1").arg(QString::fromStdString(obj.toDisplayString())), "https://www.sqlite.org/lang_createview.html", @@ -1235,26 +1247,50 @@ void MainWindow::executeQuery() // Prepare a lambda function for logging the results of a query auto logged_queries = std::make_shared(); auto query_logger = [sqlWidget, editor, logged_queries](bool ok, const QString& status_message, int from_position, int to_position) { + int editor_length = editor->length(); + int lines = editor->lines(); + if(editor_length == 0 || lines == 0) + { + sqlWidget->finishExecution(status_message, ok); + return; + } + + from_position = qBound(0, from_position, editor_length); + to_position = qBound(0, to_position, editor_length); + + // Clamp a (line, index) pair from lineIndexFromPosition to valid editor bounds. + auto clampToEditor = [&](int& line, int& index) { + line = qMin(line, lines - 1); + index = qMin(index, editor->text(line).size()); + }; + int execute_from_line, execute_from_index; editor->lineIndexFromPosition(from_position, &execute_from_line, &execute_from_index); + clampToEditor(execute_from_line, execute_from_index); // Special case: if the start position is at the end of a line, then move to the beginning of next line. // Otherwise for the typical case, the line reference is one less than expected. // Note that execute_from_index uses character positions and not byte positions, so at() can be used. - QChar char_at_index = editor->text(execute_from_line).at(execute_from_index); - if (char_at_index == '\r' || char_at_index == '\n') { - execute_from_line++; - // The next lines could be empty, so skip all of them too. - while(editor->text(execute_from_line).trimmed().isEmpty()) + QString line_text = editor->text(execute_from_line); + if(execute_from_index < line_text.size()) + { + QChar char_at_index = line_text.at(execute_from_index); + if (char_at_index == '\r' || char_at_index == '\n') { execute_from_line++; - execute_from_index = 0; + // The next lines could be empty, so skip all of them too. + while(execute_from_line < lines && editor->text(execute_from_line).trimmed().isEmpty()) + execute_from_line++; + execute_from_index = 0; + } } + clampToEditor(execute_from_line, execute_from_index); // If there was an error highlight the erroneous SQL statement if(!ok) { int end_of_current_statement_line, end_of_current_statement_index; editor->lineIndexFromPosition(to_position, &end_of_current_statement_line, &end_of_current_statement_index); + clampToEditor(end_of_current_statement_line, end_of_current_statement_index); editor->setErrorIndicator(execute_from_line, execute_from_index, end_of_current_statement_line, end_of_current_statement_index); editor->setCursorPosition(execute_from_line, execute_from_index); @@ -1262,7 +1298,9 @@ void MainWindow::executeQuery() // Log the query and the result message. // The query takes the last placeholder as it may itself contain the sequence '%' + number. - QString query = editor->text(from_position, to_position); + QString query; + if(from_position < to_position) + query = editor->text(from_position, to_position); QString log_message = "-- " + tr("At line %1:").arg(execute_from_line+1) + "\n" + query.trimmed() + "\n-- " + tr("Result: %1").arg(status_message); logged_queries->append(log_message + "\n"); @@ -2462,6 +2500,14 @@ void MainWindow::reloadSettings() ui->actionDropQualifiedCheck->setChecked(Settings::getValue("SchemaDock", "dropQualifiedNames").toBool()); ui->actionEnquoteNamesCheck->setChecked(Settings::getValue("SchemaDock", "dropEnquotedNames").toBool()); + + if (Settings::getValue("db", "watcher").toBool()) + connect(&fileSystemWatch, &QFileSystemWatcher::fileChanged, this, &MainWindow::refreshDb, Qt::UniqueConnection); + else { + disconnect(&fileSystemWatch, &QFileSystemWatcher::fileChanged, nullptr, nullptr); + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); + } } void MainWindow::checkNewVersion(const bool automatic) @@ -3267,6 +3313,11 @@ void MainWindow::saveProject(const QString& currentFilename) xml.writeAttribute("name", QString::fromStdString(tableIt->first.name())); auto obj = db.getTableByName(tableIt->first); + if(!obj) + { + xml.writeEndElement(); + continue; + } saveBrowseDataTableSettings(tableIt->second, obj, xml); xml.writeEndElement(); } diff --git a/src/MainWindow.h b/src/MainWindow.h index e8c696276..75d5e16c6 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -6,7 +6,9 @@ #include #include + #include +#include struct BrowseDataTableSettings; class DbStructureModel; @@ -121,6 +123,8 @@ friend TableBrowserDock; QString currentProjectFilename; bool isProjectModified; + QFileSystemWatcher fileSystemWatch; + void init(); void clearCompleterModelsFields(); @@ -176,6 +180,7 @@ private slots: void fileNewInMemoryDatabase(bool open_create_dialog = true); // Refresh visible table browsers. When all is true, refresh all browsers. void refreshTableBrowsers(bool all = false); + void refreshDb(); bool fileClose(); bool fileSaveAs(); void createTable(); diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index 9bd0c7a43..7c98f9905 100644 --- a/src/PreferencesDialog.cpp +++ b/src/PreferencesDialog.cpp @@ -99,6 +99,7 @@ void PreferencesDialog::loadSettings() } ui->spinStructureFontSize->setValue(Settings::getValue("db", "fontsize").toInt()); + ui->watcherCheckBox->setChecked(Settings::getValue("db", "watcher").toBool()); // Gracefully handle the preferred Data Browser font not being available int matchingFont = ui->comboDataBrowserFont->findText(Settings::getValue("databrowser", "font").toString(), Qt::MatchExactly); @@ -119,6 +120,7 @@ void PreferencesDialog::loadSettings() ui->spinSymbolLimit->setValue(Settings::getValue("databrowser", "symbol_limit").toInt()); ui->spinCompleteThreshold->setValue(Settings::getValue("databrowser", "complete_threshold").toInt()); ui->checkShowImagesInline->setChecked(Settings::getValue("databrowser", "image_preview").toBool()); + ui->checkCellWordWrap->setChecked(Settings::getValue("databrowser", "cell_word_wrap").toBool()); ui->txtNull->setText(Settings::getValue("databrowser", "null_text").toString()); ui->txtBlob->setText(Settings::getValue("databrowser", "blob_text").toString()); ui->editFilterEscape->setText(Settings::getValue("databrowser", "filter_escape").toString()); @@ -196,12 +198,14 @@ void PreferencesDialog::saveSettings(bool accept) Settings::setValue("db", "defaultsqltext", ui->editDatabaseDefaultSqlText->text()); Settings::setValue("db", "defaultfieldtype", ui->defaultFieldTypeComboBox->currentIndex()); Settings::setValue("db", "fontsize", ui->spinStructureFontSize->value()); + Settings::setValue("db", "watcher", ui->watcherCheckBox->isChecked()); Settings::setValue("checkversion", "enabled", ui->checkUpdates->isChecked()); Settings::setValue("databrowser", "font", ui->comboDataBrowserFont->currentText()); Settings::setValue("databrowser", "fontsize", ui->spinDataBrowserFontSize->value()); Settings::setValue("databrowser", "image_preview", ui->checkShowImagesInline->isChecked()); + Settings::setValue("databrowser", "cell_word_wrap", ui->checkCellWordWrap->isChecked()); saveColorSetting(ui->fr_null_fg, "null_fg"); saveColorSetting(ui->fr_null_bg, "null_bg"); saveColorSetting(ui->fr_reg_fg, "reg_fg"); diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui index 873489f30..2419f68e2 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -739,6 +739,29 @@ in new project file + + + + When the database is open in readonly mode, watch the database file and refresh when it is updated. + + + &When readonly, refresh on changed file + + + watcherCheckBox + + + + + + + enabled + + + true + + + @@ -1212,6 +1235,16 @@ in new project file + + + + Enable word wrap in cell + + + checkCellWordWrap + + + @@ -1255,6 +1288,13 @@ Can be set to 0 for disabling completion. + + + + Enable this option to turn on word wrap in the cells. + + + @@ -1981,6 +2021,7 @@ Can be set to 0 for disabling completion. spinSymbolLimit spinCompleteThreshold checkShowImagesInline + checkCellWordWrap spinFilterDelay editFilterEscape treeSyntaxHighlighting diff --git a/src/Settings.cpp b/src/Settings.cpp index 24d29098c..6fe1b5d7f 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -159,6 +159,10 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& if(group == "db" && name == "fontsize") return 10; + // db/watcher? + if(group == "db" && name == "watcher") + return false; + // exportcsv/firstrowheader? if(group == "exportcsv" && name == "firstrowheader") return true; @@ -303,6 +307,8 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& return 1000; if(name == "image_preview") return false; + if(name == "cell_word_wrap") + return true; if(name == "indent_compact") return false; if (name == "sort_keys") diff --git a/src/TableBrowser.cpp b/src/TableBrowser.cpp index 6dc3e8f2e..f78d367b4 100644 --- a/src/TableBrowser.cpp +++ b/src/TableBrowser.cpp @@ -897,15 +897,18 @@ void TableBrowser::applyViewportSettings(const BrowseDataTableSettings& storedDa // Show/hide some menu options depending on whether this is a table or a view const auto table = db->getTableByName(tablename); - if(!table->isView()) + if(table) { - // Table - ui->actionUnlockViewEditing->setVisible(false); - ui->actionShowRowidColumn->setVisible(!table->withoutRowidTable()); - } else { - // View - ui->actionUnlockViewEditing->setVisible(true); - ui->actionShowRowidColumn->setVisible(false); + if(!table->isView()) + { + // Table + ui->actionUnlockViewEditing->setVisible(false); + ui->actionShowRowidColumn->setVisible(!table->withoutRowidTable()); + } else { + // View + ui->actionUnlockViewEditing->setVisible(true); + ui->actionShowRowidColumn->setVisible(false); + } } // Frozen columns @@ -990,8 +993,11 @@ void TableBrowser::generateFilters() FilterTableHeader* filterHeader = qobject_cast(ui->dataTable->horizontalHeader()); bool oldState = filterHeader->blockSignals(true); auto obj = db->getTableByName(currentlyBrowsedTableName()); - for(auto filterIt=settings.filterValues.cbegin();filterIt!=settings.filterValues.cend();++filterIt) - ui->dataTable->setFilter(sqlb::getFieldNumber(obj, filterIt->first) + 1, filterIt->second); + if(obj) + { + for(auto filterIt=settings.filterValues.cbegin();filterIt!=settings.filterValues.cend();++filterIt) + ui->dataTable->setFilter(sqlb::getFieldNumber(obj, filterIt->first) + 1, filterIt->second); + } filterHeader->blockSignals(oldState); ui->actionClearFilters->setEnabled(m_model->filterCount() > 0 || !ui->editGlobalFilter->text().isEmpty()); @@ -1447,7 +1453,10 @@ void TableBrowser::editDisplayFormat() // column is always the rowid column. Ultimately, get the column name from the column object sqlb::ObjectIdentifier current_table = currentlyBrowsedTableName(); size_t field_number = sender()->property("clicked_column").toUInt(); - QString field_name = QString::fromStdString(db->getTableByName(current_table)->fields.at(field_number-1).name()); + auto tbl = db->getTableByName(current_table); + if(!tbl || field_number < 1 || field_number > tbl->fields.size()) + return; + QString field_name = QString::fromStdString(tbl->fields.at(field_number-1).name()); // Get the current display format of the field QString current_displayformat = m_settings[current_table].displayFormats[field_number]; @@ -1548,7 +1557,10 @@ void TableBrowser::setDefaultTableEncoding() void TableBrowser::copyColumnName(){ sqlb::ObjectIdentifier current_table = currentlyBrowsedTableName(); int col_index = ui->actionBrowseTableEditDisplayFormat->property("clicked_column").toInt(); - QString field_name = QString::fromStdString(db->getTableByName(current_table)->fields.at(col_index - 1).name()); + auto tbl = db->getTableByName(current_table); + if(!tbl || col_index < 1 || col_index > static_cast(tbl->fields.size())) + return; + QString field_name = QString::fromStdString(tbl->fields.at(col_index - 1).name()); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(field_name); diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 6e50c858d..79a04824e 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -25,6 +25,32 @@ #include #include +namespace { + +bool equalsIgnoreCase(const std::string& lhs, const std::string& rhs) +{ + if(lhs.size() != rhs.size()) + return false; + + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), [](unsigned char a, unsigned char b) { + return std::tolower(a) == std::tolower(b); + }); +} + +template +typename MapType::const_iterator findCaseInsensitive(const MapType& map, const std::string& key) +{ + auto it = map.find(key); + if(it != map.end()) + return it; + + return std::find_if(map.begin(), map.end(), [&key](const auto& entry) { + return equalsIgnoreCase(entry.first, key); + }); +} + +} + const QStringList DBBrowserDB::journalModeValues = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}; const QStringList DBBrowserDB::lockingModeValues = {"NORMAL", "EXCLUSIVE"}; @@ -913,9 +939,14 @@ bool DBBrowserDB::dump(const QString& filePath, else { QString statement = QString::fromStdString(it->originalSql()); if(keepOldSchema) { - // The statement is guaranteed by SQLite to start with "CREATE TABLE" - const int createTableLength = 12; - statement.replace(0, createTableLength, "CREATE TABLE IF NOT EXISTS"); + if (statement.startsWith("CREATE VIRTUAL TABLE", Qt::CaseInsensitive)) { + const int createTableLength = 20; + statement.replace(0, createTableLength, "CREATE VIRTUAL TABLE IF NOT EXISTS"); + } else { + // The statement is guaranteed by SQLite to start with "CREATE TABLE" + const int createTableLength = 12; + statement.replace(0, createTableLength, "CREATE TABLE IF NOT EXISTS"); + } } stream << statement << ";\n"; } @@ -1302,7 +1333,10 @@ bool DBBrowserDB::getRow(const sqlb::ObjectIdentifier& table, const QString& row std::string query = "SELECT * FROM " + table.toString() + " WHERE "; // For a single rowid column we can use a simple WHERE condition, for multiple rowid columns we have to use sqlb_make_single_value to decode the composed rowid values. - sqlb::StringVector pks = getTableByName(table)->rowidColumns(); + auto tbl = getTableByName(table); + if(!tbl) + return false; + sqlb::StringVector pks = tbl->rowidColumns(); if(pks.size() == 1) query += sqlb::escapeIdentifier(pks.front()) + "='" + rowid.toStdString() + "'"; else @@ -1344,14 +1378,18 @@ unsigned long DBBrowserDB::max(const sqlb::ObjectIdentifier& tableName, const st // If, however, there is a sequence table in this database and the given column is the primary key of the table, we try to look up a value in the sequence table if(schemata.at(tableName.schema()).tables.count("sqlite_sequence")) { - auto pk = getTableByName(tableName)->primaryKeyColumns(); - if(pk.size() == 1 && pk.front().name() == field) + auto tbl = getTableByName(tableName); + if(tbl) { - // This SQL statement tries to do two things in one statement: get the current sequence number for this table from the sqlite_sequence table or, if there is no record for the table, return the highest integer value in the given column. - // This works by querying the sqlite_sequence table and using an aggregate function (SUM in this case) to make sure to always get exactly one result row, no matter if there is a sequence record or not. We then let COALESCE decide - // whether to return that sequence value if there is one or fall back to the SELECT MAX statement from avove if there is no sequence value. - query = "SELECT COALESCE(SUM(seq), (" + query + ")) FROM sqlite_sequence WHERE name=" + sqlb::escapeString(tableName.name()); - } + auto pk = tbl->primaryKeyColumns(); + if(pk.size() == 1 && pk.front().name() == field) + { + // This SQL statement tries to do two things in one statement: get the current sequence number for this table from the sqlite_sequence table or, if there is no record for the table, return the highest integer value in the given column. + // This works by querying the sqlite_sequence table and using an aggregate function (SUM in this case) to make sure to always get exactly one result row, no matter if there is a sequence record or not. We then let COALESCE decide + // whether to return that sequence value if there is one or fall back to the SELECT MAX statement from avove if there is no sequence value. + query = "SELECT COALESCE(SUM(seq), (" + query + ")) FROM sqlite_sequence WHERE name=" + sqlb::escapeString(tableName.name()); + } + } } return querySingleValueFromDb(query).toULong(); @@ -1758,7 +1796,9 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb if(changed_something) { updateSchema(); - old_table = *getTableByName(sqlb::ObjectIdentifier(tablename.schema(), new_table.name())); + auto newTbl = getTableByName(sqlb::ObjectIdentifier(tablename.schema(), new_table.name())); + if(newTbl) + old_table = *newTbl; } // Check if there's still more work to be done or if we are finished now @@ -2220,6 +2260,66 @@ std::vector> DBBrowserDB::queryColumnInforma return result; } +const sqlb::TablePtr DBBrowserDB::getTableByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::TablePtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::TablePtr{}; + + const auto& tables = schemaIt->second.tables; + if(tables.empty()) + return sqlb::TablePtr{}; + + const auto tableIt = findCaseInsensitive(tables, name.name()); + if(tableIt == tables.end()) + return sqlb::TablePtr{}; + + return tableIt->second; +} + +const sqlb::IndexPtr DBBrowserDB::getIndexByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::IndexPtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::IndexPtr{}; + + const auto& indices = schemaIt->second.indices; + if(indices.empty()) + return sqlb::IndexPtr{}; + + const auto indexIt = findCaseInsensitive(indices, name.name()); + if(indexIt == indices.end()) + return sqlb::IndexPtr{}; + + return indexIt->second; +} + +const sqlb::TriggerPtr DBBrowserDB::getTriggerByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::TriggerPtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::TriggerPtr{}; + + const auto& triggers = schemaIt->second.triggers; + if(triggers.empty()) + return sqlb::TriggerPtr{}; + + const auto triggerIt = findCaseInsensitive(triggers, name.name()); + if(triggerIt == triggers.end()) + return sqlb::TriggerPtr{}; + + return triggerIt->second; +} + std::string DBBrowserDB::generateSavepointName(const std::string& identifier) const { // Generate some sort of unique name for a savepoint for internal use. diff --git a/src/sqlitedb.h b/src/sqlitedb.h index d80dd61ab..284ce3411 100644 --- a/src/sqlitedb.h +++ b/src/sqlitedb.h @@ -228,35 +228,14 @@ class DBBrowserDB : public QObject */ bool alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb::Table& new_table, AlterTableTrackColumns track_columns, std::string newSchemaName = std::string()); - const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty() || !schemata.count(name.schema())) - return sqlb::TablePtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.tables.count(name.name())) - return schema.tables.at(name.name()); - return sqlb::TablePtr{}; - } - - const sqlb::IndexPtr getIndexByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty()) - return sqlb::IndexPtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.indices.count(name.name())) - return schema.indices.at(name.name()); - return sqlb::IndexPtr{}; - } - - const sqlb::TriggerPtr getTriggerByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty()) - return sqlb::TriggerPtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.triggers.count(name.name())) - return schema.triggers.at(name.name()); - return sqlb::TriggerPtr{}; - } + // Given that sqlite3 does not allow case-differing identifiers, and in some + // situations the stored name in `schemata` may differ only in case from the name used in + // the stored SQL create statement (like issue #4110), the search of these get*Name + // functions is case-insensitive, if not found as is. + + const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const; + const sqlb::IndexPtr getIndexByName(const sqlb::ObjectIdentifier& name) const; + const sqlb::TriggerPtr getTriggerByName(const sqlb::ObjectIdentifier& name) const; bool isOpen() const; bool encrypted() const { return isEncrypted; } diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index 1e01b2ca0..377e93ac7 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -132,6 +132,12 @@ void SqliteTableModel::setQuery(const sqlb::Query& query) // Set the row id columns m_table_of_query = m_db.getTableByName(query.table()); + if(!m_table_of_query) + { + // Table not found in schema (e.g. case mismatch between sqlite_master and SQL statement, or table dropped). + // Cannot proceed without table metadata. + return; + } if(!m_table_of_query->isView()) { // It is a table