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