Refactor the way we store a database schema

This commit does a lot of refactoring. But most noticeably it changes
two things:

1) Instead of saving all objects (tables, views, indices, triggers) of a
   schema in a common map, we now store tables/views, indices and
   triggers in three separate maps. This has a number of benefits:
   - It resembles more closely how SQLite stores its data internally and
     therefore achieves greater compatability e.g. for databases with a
     view and a trigger with the same name.
   - It reduces the need for runtime polymorphism. This makes the code
     run a bit faster.
   - By working with explicit types more often more error checking can
     be done at compile time, making the code less error prone.
   - The code becomes a lot clearer to read.

2) By making View inherit form Table, views are now a sort of tables.
   This has the following benefits:
   - This is a again how SQLite stores views internally which again
     should increase compatibility a bit.
   - We mostly treat views and tables the same anyway and with these
     changes we can unify the code for them even more.
This commit is contained in:
Martin Kleusberg
2021-01-29 17:18:09 +01:00
parent 13ab455d5c
commit 02db68107a
17 changed files with 324 additions and 418 deletions

View File

@@ -189,25 +189,22 @@ void AddRecordDialog::populateFields()
bool auto_increment = false;
// Initialize fields, fks and pk differently depending on whether it's a table or a view.
const sqlb::ObjectPtr obj = pdb.getObjectByName(curTable);
if (obj->type() == sqlb::Object::Table)
const sqlb::TablePtr table = pdb.getTableByName(curTable);
fields = table->fields;
if (!table->isView())
{
sqlb::TablePtr m_table = pdb.getObjectByName<sqlb::Table>(curTable);
fields = m_table->fields;
std::transform(fields.begin(), fields.end(), std::back_inserter(fks), [m_table](const auto& f) {
return m_table->constraint({f.name()}, sqlb::Constraint::ForeignKeyConstraintType);
std::transform(fields.begin(), fields.end(), std::back_inserter(fks), [table](const auto& f) {
return table->constraint({f.name()}, sqlb::Constraint::ForeignKeyConstraintType);
});
const auto pk_constraint = m_table->primaryKey();
const auto pk_constraint = table->primaryKey();
if(pk_constraint)
{
pk = pk_constraint->columnList();
auto_increment = pk_constraint->autoIncrement();
}
} else {
sqlb::ViewPtr m_view = pdb.getObjectByName<sqlb::View>(curTable);
fields = m_view->fields;
// It's a view
fks.resize(fields.size(), sqlb::ConstraintPtr(nullptr));
pk = pseudo_pk;
}

View File

@@ -8,8 +8,6 @@
#include <QMimeData>
#include <QMessageBox>
#include <QApplication>
#include <unordered_map>
#include <map>
DbStructureModel::DbStructureModel(DBBrowserDB& db, QObject* parent)
: QAbstractItemModel(parent),
@@ -304,108 +302,78 @@ bool DbStructureModel::dropMimeData(const QMimeData* data, Qt::DropAction action
}
}
static long calc_number_of_objects_by_type(const objectMap& objmap, const std::string& type)
{
auto objects = objmap.equal_range(type);
if(objects.first == objmap.end())
return 0;
else
return std::distance(objects.first, objects.second);
}
void DbStructureModel::buildTree(QTreeWidgetItem* parent, const std::string& schema)
{
// Build a map from object type to tree node to simplify finding the correct tree node later
std::unordered_map<std::string, QTreeWidgetItem*> typeToParentItem;
// Get object map for the given schema
objectMap objmap = m_db.schemata[schema];
const objectMap& objmap = m_db.schemata.at(schema);
// Prepare tree
auto num_tables = std::count_if(objmap.tables.begin(), objmap.tables.end(), [](const auto& t) { return !t.second->isView(); });
QTreeWidgetItem* itemTables = new QTreeWidgetItem(parent);
itemTables->setIcon(ColumnName, IconCache::get("table"));
itemTables->setText(ColumnName, tr("Tables (%1)").arg(calc_number_of_objects_by_type(objmap, "table")));
typeToParentItem.insert({"table", itemTables});
itemTables->setText(ColumnName, tr("Tables (%1)").arg(num_tables));
QTreeWidgetItem* itemIndices = new QTreeWidgetItem(parent);
itemIndices->setIcon(ColumnName, IconCache::get("index"));
itemIndices->setText(ColumnName, tr("Indices (%1)").arg(calc_number_of_objects_by_type(objmap, "index")));
typeToParentItem.insert({"index", itemIndices});
itemIndices->setText(ColumnName, tr("Indices (%1)").arg(objmap.indices.size()));
auto num_views = std::count_if(objmap.tables.begin(), objmap.tables.end(), [](const auto& t) { return t.second->isView(); });
QTreeWidgetItem* itemViews = new QTreeWidgetItem(parent);
itemViews->setIcon(ColumnName, IconCache::get("view"));
itemViews->setText(ColumnName, tr("Views (%1)").arg(calc_number_of_objects_by_type(objmap, "view")));
typeToParentItem.insert({"view", itemViews});
itemViews->setText(ColumnName, tr("Views (%1)").arg(num_views));
QTreeWidgetItem* itemTriggers = new QTreeWidgetItem(parent);
itemTriggers->setIcon(ColumnName, IconCache::get("trigger"));
itemTriggers->setText(ColumnName, tr("Triggers (%1)").arg(calc_number_of_objects_by_type(objmap, "trigger")));
typeToParentItem.insert({"trigger", itemTriggers});
itemTriggers->setText(ColumnName, tr("Triggers (%1)").arg(objmap.triggers.size()));
// Get all database objects and sort them by their name.
// This needs to be a multimap because SQLite allows views and triggers with the same name which means that names can appear twice.
std::multimap<std::string, sqlb::ObjectPtr> dbobjs;
for(const auto& it : objmap)
dbobjs.insert({it.second->name(), it.second});
// Add the database objects to the tree nodes
for(const auto& obj : dbobjs)
// Add tables and views
for(const auto& obj : objmap.tables)
{
sqlb::ObjectPtr it = obj.second;
QTreeWidgetItem* item = addNode(schema, obj.second->name(), obj.second->isView() ? "view" : "table", obj.second->originalSql(), obj.second->isView() ? itemViews : itemTables);
// Object node
QTreeWidgetItem* item = addNode(typeToParentItem.at(sqlb::Object::typeToString(it->type())), it, schema);
// Add an extra node for the browsable section
addNode(schema, obj.second->name(), obj.second->isView() ? "view" : "table", obj.second->originalSql(), browsablesRootItem);
// If it is a table or view add the field nodes, add an extra node for the browsable section
if(it->type() == sqlb::Object::Types::Table || it->type() == sqlb::Object::Types::View)
addNode(browsablesRootItem, it, schema);
// Add field nodes if there are any
sqlb::FieldInfoList fieldList = it->fieldInformation();
if(!fieldList.empty())
sqlb::StringVector pk_columns;
if(!obj.second->isView())
{
sqlb::StringVector pk_columns;
if(it->type() == sqlb::Object::Types::Table)
{
const auto pk = std::dynamic_pointer_cast<sqlb::Table>(it)->primaryKey();
if(pk)
pk_columns = pk->columnList();
}
const auto pk = obj.second->primaryKey();
if(pk)
pk_columns = pk->columnList();
}
for(const sqlb::FieldInfo& field : fieldList)
{
QTreeWidgetItem *fldItem = new QTreeWidgetItem(item);
bool isFK = false;
if(it->type() == sqlb::Object::Types::Table)
isFK = std::dynamic_pointer_cast<sqlb::Table>(it)->constraint({field.name}, sqlb::Constraint::ForeignKeyConstraintType) != nullptr;
for(const auto& field : obj.second->fields)
{
bool isPK = contains(pk_columns, field.name());
bool isFK = obj.second->constraint({field.name()}, sqlb::Constraint::ForeignKeyConstraintType) != nullptr;
fldItem->setText(ColumnName, QString::fromStdString(field.name));
fldItem->setText(ColumnObjectType, "field");
fldItem->setText(ColumnDataType, QString::fromStdString(field.type));
fldItem->setText(ColumnSQL, QString::fromStdString(field.sql));
fldItem->setText(ColumnSchema, QString::fromStdString(schema));
if(contains(pk_columns, field.name))
fldItem->setIcon(ColumnName, IconCache::get("field_key"));
else if(isFK)
fldItem->setIcon(ColumnName, IconCache::get("field_fk"));
else
fldItem->setIcon(ColumnName, IconCache::get("field"));
}
addNode(schema, field.name(), "field", field.toString(" ", " "), item, field.type(), isPK ? "_key" : (isFK ? "_fk" : std::string{}));
}
}
// Add indices
for(const auto& obj : objmap.indices)
{
QTreeWidgetItem* item = addNode(schema, obj.second->name(), "index", obj.second->originalSql(), itemIndices);
for(const auto& field : obj.second->fields)
addNode(schema, field.name(), "field", field.toString(" ", " "), item, field.order());
}
// Add triggers
for(const auto& obj : objmap.triggers)
addNode(schema, obj.second->name(), "trigger", obj.second->originalSql(), itemTriggers);
}
QTreeWidgetItem* DbStructureModel::addNode(QTreeWidgetItem* parent, const sqlb::ObjectPtr& object, const std::string& schema)
QTreeWidgetItem* DbStructureModel::addNode(const std::string& schema, const std::string& name, const std::string& object_type, const std::string& sql, QTreeWidgetItem* parent_item, const std::string& data_type, const std::string& icon_suffix)
{
std::string type = sqlb::Object::typeToString(object->type());
QTreeWidgetItem *item = new QTreeWidgetItem(parent);
item->setIcon(ColumnName, IconCache::get(type));
item->setText(ColumnName, QString::fromStdString(object->name()));
item->setText(ColumnObjectType, QString::fromStdString(type));
item->setText(ColumnSQL, QString::fromStdString(object->originalSql()));
QTreeWidgetItem* item = new QTreeWidgetItem(parent_item);
item->setText(ColumnName, QString::fromStdString(name));
item->setText(ColumnObjectType, QString::fromStdString(object_type));
item->setText(ColumnDataType, QString::fromStdString(data_type));
item->setText(ColumnSQL, QString::fromStdString(sql));
item->setText(ColumnSchema, QString::fromStdString(schema));
item->setIcon(ColumnName, IconCache::get(object_type + icon_suffix));
return item;
}

View File

@@ -6,7 +6,6 @@
class DBBrowserDB;
class QTreeWidgetItem;
namespace sqlb { class Object; using ObjectPtr = std::shared_ptr<Object>; }
class DbStructureModel : public QAbstractItemModel
{
@@ -50,7 +49,7 @@ private:
bool m_dropEnquotedNames;
void buildTree(QTreeWidgetItem* parent, const std::string& schema);
QTreeWidgetItem* addNode(QTreeWidgetItem* parent, const sqlb::ObjectPtr& object, const std::string& schema);
QTreeWidgetItem* addNode(const std::string& schema, const std::string& name, const std::string& object_type, const std::string& sql, QTreeWidgetItem* parent_item, const std::string& data_type = {}, const std::string& icon_suffix = {});
QString getNameForDropping(const QString& domain, const QString& object, const QString& field) const;
};

View File

@@ -25,20 +25,18 @@ EditIndexDialog::EditIndexDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
{
for(const auto& it : pdb.schemata)
{
auto tables = it.second.equal_range("table");
for(auto jt=tables.first;jt!=tables.second;++jt)
for(const auto& jt : it.second.tables)
{
// Only show the schema name for non-main schemata
sqlb::ObjectIdentifier obj(it.first, jt->second->name());
sqlb::ObjectIdentifier obj(it.first, jt.first);
dbobjs.insert({obj.toDisplayString(), obj});
}
}
} else { // If this is an existing index, only offer tables of the current database schema
auto tables = pdb.schemata[curIndex.schema()].equal_range("table");
for(auto it=tables.first;it!=tables.second;++it)
for(const auto& it : pdb.schemata[curIndex.schema()].tables)
{
// Only show the schema name for non-main schemata
sqlb::ObjectIdentifier obj(curIndex.schema(), it->second->name());
sqlb::ObjectIdentifier obj(curIndex.schema(), it.first);
dbobjs.insert({obj.toDisplayString(), obj});
}
}
@@ -54,7 +52,7 @@ EditIndexDialog::EditIndexDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
if(!newIndex)
{
// Load the current layout and fill in the dialog fields
index = *(pdb.getObjectByName<sqlb::Index>(curIndex));
index = *pdb.getIndexByName(curIndex);
ui->editIndexName->blockSignals(true);
ui->editIndexName->setText(QString::fromStdString(index.name()));
@@ -114,26 +112,25 @@ void EditIndexDialog::tableChanged(const QString& new_table, bool initialLoad)
void EditIndexDialog::updateColumnLists()
{
// Fill the table column list
sqlb::TablePtr table = pdb.getObjectByName<sqlb::Table>(sqlb::ObjectIdentifier(ui->comboTableName->currentData().toString().toStdString()));
sqlb::TablePtr table = pdb.getTableByName(sqlb::ObjectIdentifier(ui->comboTableName->currentData().toString().toStdString()));
if(!table)
return;
sqlb::FieldInfoList tableFields = table->fieldInformation();
ui->tableTableColumns->setRowCount(static_cast<int>(tableFields.size()));
ui->tableTableColumns->setRowCount(static_cast<int>(table->fields.size()));
int tableRows = 0;
for(size_t i=0;i<tableFields.size();++i)
for(const auto& field: table->fields)
{
// When we're doing the initial loading and this field already is in the index to edit, then don't add it to the
// list of table columns. It will be added to the list of index columns in the next step. When this is not the initial
// loading, the index column list is empty, so this check will always be true.
if(sqlb::findField(index, tableFields.at(i).name) == index.fields.end())
if(sqlb::findField(index, field.name()) == index.fields.end())
{
// Put the name of the field in the first column
QTableWidgetItem* name = new QTableWidgetItem(QString::fromStdString(tableFields.at(i).name));
QTableWidgetItem* name = new QTableWidgetItem(QString::fromStdString(field.name()));
name->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
ui->tableTableColumns->setItem(tableRows, 0, name);
// Put the data type in the second column
QTableWidgetItem* type = new QTableWidgetItem(QString::fromStdString(tableFields.at(i).type));
QTableWidgetItem* type = new QTableWidgetItem(QString::fromStdString(field.type()));
type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
ui->tableTableColumns->setItem(tableRows, 1, type);

View File

@@ -78,7 +78,7 @@ EditTableDialog::EditTableDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
if(m_bNewTable == false)
{
// Existing table, so load and set the current layout
m_table = *(pdb.getObjectByName<sqlb::Table>(curTable));
m_table = *pdb.getTableByName(curTable);
ui->labelEditWarning->setVisible(!m_table.fullyParsed());
// Initialise the list of tracked columns for table layout changes
@@ -492,13 +492,11 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
if(!m_bNewTable)
{
const auto pk = m_table.primaryKey();
const auto tables = pdb.schemata[curTable.schema()].equal_range("table");
for(auto it=tables.first;it!=tables.second;++it)
for(const auto& it : pdb.schemata[curTable.schema()].tables)
{
const sqlb::ObjectPtr& fkobj = it->second;
const sqlb::TablePtr& fkobj = it.second;
auto fks = std::dynamic_pointer_cast<sqlb::Table>(fkobj)->constraints({}, sqlb::Constraint::ForeignKeyConstraintType);
auto fks = fkobj->constraints({}, sqlb::Constraint::ForeignKeyConstraintType);
for(const sqlb::ConstraintPtr& fkptr : fks)
{
auto fk = std::dynamic_pointer_cast<sqlb::ForeignKeyClause>(fkptr);
@@ -591,7 +589,7 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
// to at least replace all troublesome NULL values by the default value
SqliteTableModel m(pdb, this);
m.setQuery(QString("SELECT COUNT(%1) FROM %2 WHERE coalesce(NULL,%3) IS NULL;").arg(
QString::fromStdString(sqlb::joinStringVector(sqlb::escapeIdentifier(pdb.getObjectByName<sqlb::Table>(curTable)->rowidColumns()), ",")),
QString::fromStdString(sqlb::joinStringVector(sqlb::escapeIdentifier(pdb.getTableByName(curTable)->rowidColumns()), ",")),
QString::fromStdString(curTable.toString()),
QString::fromStdString(sqlb::escapeIdentifier(field.name()))));
if(!m.completeCache())

View File

@@ -47,18 +47,10 @@ ExportDataDialog::ExportDataDialog(DBBrowserDB& db, ExportFormats format, QWidge
// Get list of tables to export
for(const auto& it : pdb.schemata)
{
const auto tables = it.second.equal_range("table");
const auto views = it.second.equal_range("view");
std::map<std::string, sqlb::ObjectPtr> objects;
for(auto jt=tables.first;jt!=tables.second;++jt)
objects.insert({jt->second->name(), jt->second});
for(auto jt=views.first;jt!=views.second;++jt)
objects.insert({jt->second->name(), jt->second});
for(const auto& jt : objects)
for(const auto& jt : it.second.tables)
{
sqlb::ObjectIdentifier obj(it.first, jt.second->name());
QListWidgetItem* item = new QListWidgetItem(IconCache::get(sqlb::Object::typeToString(jt.second->type())), QString::fromStdString(obj.toDisplayString()));
sqlb::ObjectIdentifier obj(it.first, jt.first);
QListWidgetItem* item = new QListWidgetItem(IconCache::get(jt.second->isView() ? "view" : "table"), QString::fromStdString(obj.toDisplayString()));
item->setData(Qt::UserRole, QString::fromStdString(obj.toSerialised()));
ui->listTables->addItem(item);
}

View File

@@ -29,9 +29,8 @@ ExportSqlDialog::ExportSqlDialog(DBBrowserDB* db, QWidget* parent, const QString
ui->comboOldSchema->setCurrentIndex(Settings::getValue("exportsql", "oldschema").toInt());
// Get list of tables to export
const auto objects = pdb->schemata["main"].equal_range("table");
for(auto it=objects.first;it!=objects.second;++it)
ui->listTables->addItem(new QListWidgetItem(IconCache::get(sqlb::Object::typeToString(it->second->type())), QString::fromStdString(it->second->name())));
for(const auto& it : pdb->schemata["main"].tables)
ui->listTables->addItem(new QListWidgetItem(IconCache::get(it.second->isView() ? "view" : "table"), QString::fromStdString(it.first)));
// Sort list of tables and select the table specified in the
// selection parameter or all tables if table not specified

View File

@@ -139,12 +139,12 @@ QWidget* ExtendedTableWidgetEditorDelegate::createEditor(QWidget* parent, const
std::string column;
// If no column name is set, assume the primary key is meant
if(fk.columns().empty()) {
sqlb::TablePtr obj = m->db().getObjectByName<sqlb::Table>(foreignTable);
sqlb::TablePtr obj = m->db().getTableByName(foreignTable);
column = obj->primaryKey()->columnList().front();
} else
column = fk.columns().at(0);
sqlb::TablePtr currentTable = m->db().getObjectByName<sqlb::Table>(m->currentTableName());
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
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,
@@ -792,7 +792,7 @@ void ExtendedTableWidget::cut()
{
const QModelIndexList& indices = selectionModel()->selectedIndexes();
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
sqlb::TablePtr currentTable = m->db().getObjectByName<sqlb::Table>(m->currentTableName());
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
copy(false, false);
@@ -1164,7 +1164,7 @@ void ExtendedTableWidget::currentChanged(const QModelIndex &current, const QMode
void ExtendedTableWidget::setToNull(const QModelIndexList& indices)
{
SqliteTableModel* m = qobject_cast<SqliteTableModel*>(model());
sqlb::TablePtr currentTable = m->db().getObjectByName<sqlb::Table>(m->currentTableName());
sqlb::TablePtr currentTable = m->db().getTableByName(m->currentTableName());
// Check if some column in the selection has a NOT NULL constraint, before trying to update the cells.
if(currentTable) {

View File

@@ -83,11 +83,11 @@ ForeignKeyEditorDelegate::ForeignKeyEditorDelegate(const DBBrowserDB& db, sqlb::
{
for(const auto& it : m_db.schemata)
{
for(const auto& jt : it.second)
for(const auto& jt : it.second.tables)
{
// Don't insert the current table into the list. The name and fields of the current table are always taken from the m_table reference
if(jt.second->type() == sqlb::Object::Types::Table && jt.second->name() != m_table.name())
m_tablesIds.insert({jt.second->name(), std::dynamic_pointer_cast<sqlb::Table>(jt.second)->fieldNames()});
if(jt.first != m_table.name())
m_tablesIds.insert({jt.first, jt.second->fieldNames()});
}
}
}

View File

@@ -499,10 +499,10 @@ bool ImportCsvDialog::importCsv(const QString& fileName, const QString& name)
// Are we importing into an existing table?
bool importToExistingTable = false;
const sqlb::ObjectPtr obj = pdb->getObjectByName(sqlb::ObjectIdentifier("main", tableName.toStdString()));
if(obj && obj->type() == sqlb::Object::Types::Table)
const sqlb::TablePtr tbl = pdb->getTableByName(sqlb::ObjectIdentifier("main", tableName.toStdString()));
if(tbl)
{
if(std::dynamic_pointer_cast<sqlb::Table>(obj)->fields.size() != fieldList.size())
if(tbl->fields.size() != fieldList.size())
{
QMessageBox::warning(this, QApplication::applicationName(),
tr("There is already a table named '%1' and an import into an existing table is only possible if the number of columns match.").arg(tableName));
@@ -564,7 +564,6 @@ bool ImportCsvDialog::importCsv(const QString& fileName, const QString& name)
// Prepare the values for each table column that are to be inserted if the field in the CSV file is empty. Depending on the data type
// and the constraints of a field, we need to handle this case differently.
sqlb::TablePtr tbl = pdb->getObjectByName<sqlb::Table>(sqlb::ObjectIdentifier("main", tableName.toStdString()));
if(tbl)
{
for(const sqlb::Field& f : tbl->fields)

View File

@@ -641,16 +641,12 @@ void MainWindow::populateStructure(const std::vector<sqlb::ObjectIdentifier>& ol
{
SqlUiLexer::TablesAndColumnsMap tablesToColumnsMap;
for(const auto& jt : it.second)
for(const auto& jt : it.second.tables)
{
if(jt.second->type() == sqlb::Object::Types::Table || jt.second->type() == sqlb::Object::Types::View)
{
QString objectname = QString::fromStdString(jt.second->name());
QString objectname = QString::fromStdString(jt.first);
sqlb::FieldInfoList fi = jt.second->fieldInformation();
for(const sqlb::FieldInfo& f : fi)
tablesToColumnsMap[objectname].push_back(QString::fromStdString(f.name));
}
for(const auto& f : jt.second->fields)
tablesToColumnsMap[objectname].push_back(QString::fromStdString(f.name()));
}
qualifiedTablesMap[QString::fromStdString(it.first)] = tablesToColumnsMap;
@@ -989,13 +985,13 @@ void MainWindow::editObject()
if(dialog.exec())
refreshTableBrowsers();
} else if(type == "view") {
sqlb::ViewPtr view = db.getObjectByName<sqlb::View>(obj);
sqlb::TablePtr view = db.getTableByName(obj);
runSqlNewTab(QString("DROP VIEW %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",
/* autoRun */ false);
} else if(type == "trigger") {
sqlb::TriggerPtr trigger = db.getObjectByName<sqlb::Trigger>(obj);
sqlb::TriggerPtr trigger = db.getTriggerByName(obj);
runSqlNewTab(QString("DROP TRIGGER %1;\n%2").arg(QString::fromStdString(obj.toString()), QString::fromStdString(trigger->sql())),
tr("Edit Trigger %1").arg(QString::fromStdString(obj.toDisplayString())),
"https://www.sqlite.org/lang_createtrigger.html",
@@ -2500,10 +2496,8 @@ static void loadCondFormatMap(BrowseDataTableSettings::CondFormatMap& condFormat
}
}
static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, sqlb::ObjectPtr obj, QXmlStreamReader& xml)
static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, sqlb::TablePtr obj, QXmlStreamReader& xml)
{
const auto field_information = obj->fieldInformation();
settings.showRowid = xml.attributes().value("show_row_id").toInt();
settings.encoding = xml.attributes().value("encoding").toString();
settings.plotXAxis = xml.attributes().value("plot_x_axis").toString();
@@ -2520,8 +2514,8 @@ static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, sqlb:
{
int index = xml.attributes().value("index").toInt();
int mode = xml.attributes().value("mode").toInt();
if(static_cast<size_t>(index) < field_information.size())
settings.sortColumns.emplace_back(field_information.at(static_cast<size_t>(index)).name, mode == Qt::AscendingOrder ? sqlb::OrderBy::Ascending : sqlb::OrderBy::Descending);
if(static_cast<size_t>(index) < obj->fields.size())
settings.sortColumns.emplace_back(obj->fields.at(static_cast<size_t>(index)).name(), mode == Qt::AscendingOrder ? sqlb::OrderBy::Ascending : sqlb::OrderBy::Descending);
xml.skipCurrentElement();
}
}
@@ -2539,7 +2533,7 @@ static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, sqlb:
size_t index = xml.attributes().value("index").toUInt();
QString value = xml.attributes().value("value").toString();
if(!value.isEmpty())
settings.filterValues[field_information.at(index).name] = value;
settings.filterValues[obj->fields.at(index).name()] = value;
xml.skipCurrentElement();
}
}
@@ -2807,7 +2801,7 @@ bool MainWindow::loadProject(QString filename, bool readOnly)
sqlb::ObjectIdentifier (xml.attributes().value("schema").toString().toStdString(),
xml.attributes().value("name").toString().toStdString());
BrowseDataTableSettings settings;
loadBrowseDataTableSettings(settings, db.getObjectByName(tableIdentifier), xml);
loadBrowseDataTableSettings(settings, db.getTableByName(tableIdentifier), xml);
TableBrowser::setSettings(tableIdentifier, settings);
}
}
@@ -2893,7 +2887,7 @@ static void saveCondFormatMap(const QString& elementName, const BrowseDataTableS
xml.writeEndElement();
}
static void saveBrowseDataTableSettings(const BrowseDataTableSettings& object, sqlb::ObjectPtr obj, QXmlStreamWriter& xml)
static void saveBrowseDataTableSettings(const BrowseDataTableSettings& object, sqlb::TablePtr obj, QXmlStreamWriter& xml)
{
xml.writeAttribute("show_row_id", QString::number(object.showRowid));
xml.writeAttribute("encoding", object.encoding);
@@ -3098,7 +3092,7 @@ void MainWindow::saveProject(const QString& currentFilename)
xml.writeAttribute("schema", QString::fromStdString(tableIt->first.schema()));
xml.writeAttribute("name", QString::fromStdString(tableIt->first.name()));
auto obj = db.getObjectByName(tableIt->first);
auto obj = db.getTableByName(tableIt->first);
saveBrowseDataTableSettings(tableIt->second, obj, xml);
xml.writeEndElement();
}
@@ -3409,7 +3403,7 @@ void MainWindow::saveAsView(const std::string& query)
name = QInputDialog::getText(this, qApp->applicationName(), tr("Please specify the view name")).trimmed();
if(name.isNull())
return;
if(db.getObjectByName(sqlb::ObjectIdentifier("main", name.toStdString())) != nullptr)
if(db.getTableByName(sqlb::ObjectIdentifier("main", name.toStdString())))
QMessageBox::warning(this, qApp->applicationName(), tr("There is already an object with that name. Please choose a different name."));
else
break;
@@ -3831,15 +3825,12 @@ QList<TableBrowserDock*> MainWindow::allTableBrowserDocks() const
void MainWindow::newRowCountsTab()
{
QString sql;
for(const auto& it : db.schemata["main"])
for(const auto& it : db.schemata["main"].tables)
{
if(it.second->type() == sqlb::Object::Table || it.second->type() == sqlb::Object::View)
{
sql += QString("SELECT %1 AS \"name\", %2 AS \"type\", COUNT(*) AS \"rows\" FROM %3\nUNION ").arg(
QString::fromStdString(sqlb::escapeString(it.second->name())),
QString::fromStdString(sqlb::escapeString(it.second->typeToString(it.second->type()))),
QString::fromStdString(sqlb::escapeIdentifier(it.second->name())));
}
sql += QString("SELECT %1 AS \"name\", '%2' AS \"type\", COUNT(*) AS \"rows\" FROM %3\nUNION ").arg(
QString::fromStdString(sqlb::escapeString(it.first)),
QString::fromStdString(it.second->isView() ? "view" : "table"),
QString::fromStdString(sqlb::escapeIdentifier(it.first)));
}
sql.chop(7); // Remove the last "\nUNION " at the end

View File

@@ -725,10 +725,10 @@ void TableBrowser::updateRecordsetLabel()
sqlb::ObjectIdentifier current_table = currentlyBrowsedTableName();
if(!current_table.isEmpty() && !m_model->query().empty())
{
auto obj = db->getObjectByName(current_table);
auto obj = db->getTableByName(current_table);
is_table_or_unlocked_view = obj && (
(obj->type() == sqlb::Object::View && m_model->hasPseudoPk()) ||
(obj->type() == sqlb::Object::Table));
(obj->isView() && m_model->hasPseudoPk()) ||
(!obj->isView()));
}
enableEditing(m_model->rowCountAvailable() != SqliteTableModel::RowCount::Unknown && is_table_or_unlocked_view);
@@ -764,20 +764,19 @@ sqlb::Query TableBrowser::buildQuery(const BrowseDataTableSettings& storedData,
// Display formats
bool only_defaults = true;
const auto table = db->getObjectByName(tablename);
const auto table = db->getTableByName(tablename);
if(table)
{
// When there is at least one custom display format, we have to set all columns for the query explicitly here
const sqlb::FieldInfoList& tablefields = table->fieldInformation();
for(size_t i=0; i<tablefields.size(); ++i)
for(size_t i=0; i<table->fields.size(); ++i)
{
QString format = contains(storedData.displayFormats, i+1) ? storedData.displayFormats.at(i+1) : QString();
if(format.size())
{
query.selectedColumns().emplace_back(tablefields.at(i).name, format.toStdString());
query.selectedColumns().emplace_back(table->fields.at(i).name(), format.toStdString());
only_defaults = false;
} else {
query.selectedColumns().emplace_back(tablefields.at(i).name, tablefields.at(i).name);
query.selectedColumns().emplace_back(table->fields.at(i).name(), table->fields.at(i).name());
}
}
}
@@ -837,12 +836,12 @@ void TableBrowser::applyViewportSettings(const BrowseDataTableSettings& storedDa
ui->editGlobalFilter->setText(text);
// Show/hide some menu options depending on whether this is a table or a view
const auto table = db->getObjectByName<sqlb::Table>(tablename);
if(table)
const auto table = db->getTableByName(tablename);
if(!table->isView())
{
// Table
ui->actionUnlockViewEditing->setVisible(false);
ui->actionShowRowidColumn->setVisible(!std::dynamic_pointer_cast<sqlb::Table>(table)->withoutRowidTable());
ui->actionShowRowidColumn->setVisible(!table->withoutRowidTable());
} else {
// View
ui->actionUnlockViewEditing->setVisible(true);
@@ -930,7 +929,7 @@ void TableBrowser::generateFilters()
// Set filters blocking signals for this since the filter is already applied to the browse table model
FilterTableHeader* filterHeader = qobject_cast<FilterTableHeader*>(ui->dataTable->horizontalHeader());
bool oldState = filterHeader->blockSignals(true);
auto obj = db->getObjectByName(currentlyBrowsedTableName());
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);
filterHeader->blockSignals(oldState);
@@ -939,10 +938,10 @@ void TableBrowser::generateFilters()
void TableBrowser::unlockViewEditing(bool unlock, QString pk)
{
sqlb::ObjectIdentifier currentTable = currentlyBrowsedTableName();
sqlb::ViewPtr obj = db->getObjectByName<sqlb::View>(currentTable);
sqlb::TablePtr view = db->getTableByName(currentTable);
// If this isn't a view just unlock editing and return
if(!obj)
if(!view)
{
m_model->setPseudoPk(m_model->pseudoPk());
enableEditing(true);
@@ -957,7 +956,7 @@ void TableBrowser::unlockViewEditing(bool unlock, QString pk)
bool ok;
QStringList options;
for(const auto& n : obj->fieldNames())
for(const auto& n : view->fieldNames())
options.push_back(QString::fromStdString(n));
// Ask for a PK
@@ -1210,7 +1209,7 @@ void TableBrowser::showRecordPopupMenu(const QPoint& pos)
QMenu popupRecordMenu(this);
// "Delete and duplicate records" can only be done on writable objects
if(db->getObjectByName(currentlyBrowsedTableName())->type() == sqlb::Object::Types::Table && !db->readOnly()) {
if(db->getTableByName(currentlyBrowsedTableName()) && !db->readOnly()) {
// Select the row if it is not already in the selection.
QModelIndexList rowList = ui->dataTable->selectionModel()->selectedRows();
@@ -1382,11 +1381,8 @@ 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;
if (db->getObjectByName(current_table)->type() == sqlb::Object::Table)
field_name = QString::fromStdString(db->getObjectByName<sqlb::Table>(current_table)->fields.at(field_number-1).name());
else
field_name = QString::fromStdString(db->getObjectByName<sqlb::View>(current_table)->fieldNames().at(field_number-1));
QString field_name = QString::fromStdString(db->getTableByName(current_table)->fields.at(field_number-1).name());
// Get the current display format of the field
QString current_displayformat = m_settings[current_table].displayFormats[field_number];
@@ -1483,7 +1479,7 @@ void TableBrowser::setDefaultTableEncoding()
void TableBrowser::jumpToRow(const sqlb::ObjectIdentifier& table, std::string column, const QByteArray& value)
{
// First check if table exists
sqlb::TablePtr obj = db->getObjectByName<sqlb::Table>(table);
sqlb::TablePtr obj = db->getTableByName(table);
if(!obj)
return;

View File

@@ -35,18 +35,6 @@ bool Object::operator==(const Object& rhs) const
return true;
}
std::string Object::typeToString(Types type)
{
switch(type)
{
case Types::Table: return "table";
case Types::Index: return "index";
case Types::View: return "view";
case Types::Trigger: return "trigger";
}
return std::string();
}
ConstraintPtr Constraint::makeConstraint(ConstraintTypes type)
{
switch(type)
@@ -441,13 +429,6 @@ StringVector Table::rowidColumns() const
return {"_rowid_"};
}
FieldInfoList Table::fieldInformation() const
{
FieldInfoList result;
std::transform(fields.begin(), fields.end(), std::back_inserter(result), [](const auto& f) { return FieldInfo(f.name(), f.type(), f.toString(" ", " "), f.affinity()); });
return result;
}
TablePtr Table::parseSQL(const std::string& sSQL)
{
parser::ParserDriver drv;
@@ -458,7 +439,9 @@ TablePtr Table::parseSQL(const std::string& sSQL)
return t;
} else {
std::cerr << "Sqlite parse error: " << sSQL << std::endl;
return std::make_shared<Table>("");
TablePtr t = std::make_shared<Table>("");
t->setOriginalSql(sSQL);
return t;
}
}
@@ -672,13 +655,6 @@ std::string Index::sql(const std::string& schema, bool ifNotExists) const
return sql + ";";
}
FieldInfoList Index::fieldInformation() const
{
FieldInfoList result;
std::transform(fields.begin(), fields.end(), std::back_inserter(result), [](const auto& c) { return FieldInfo(c.name(), c.order(), c.toString(" ", " "), Field::IntegerAffinity); });
return result;
}
IndexPtr Index::parseSQL(const std::string& sSQL)
{
parser::ParserDriver drv;
@@ -689,10 +665,18 @@ IndexPtr Index::parseSQL(const std::string& sSQL)
return i;
} else {
std::cerr << "Sqlite parse error: " << sSQL << std::endl;
return std::make_shared<Index>("");
IndexPtr i = std::make_shared<Index>("");
i->setOriginalSql(sSQL);
return i;
}
}
template<>
std::string getBaseTable<Index>(IndexPtr object)
{
return object->table();
}
ViewPtr View::parseSQL(const std::string& sSQL)
@@ -704,20 +688,6 @@ ViewPtr View::parseSQL(const std::string& sSQL)
return v;
}
StringVector View::fieldNames() const
{
StringVector sl;
std::transform(fields.begin(), fields.end(), std::back_inserter(sl), [](const auto& f) { return f.name(); });
return sl;
}
FieldInfoList View::fieldInformation() const
{
FieldInfoList result;
std::transform(fields.begin(), fields.end(), std::back_inserter(result), [](const auto& f) { return FieldInfo(f.name(), f.type(), f.toString(" ", " "), f.affinity()); });
return result;
}
TriggerPtr Trigger::parseSQL(const std::string& sSQL)
{
@@ -728,4 +698,10 @@ TriggerPtr Trigger::parseSQL(const std::string& sSQL)
return t;
}
template<>
std::string getBaseTable<Trigger>(TriggerPtr object)
{
return object->table();
}
} //namespace sqlb

View File

@@ -61,7 +61,6 @@ class Trigger;
class Field;
class Constraint;
class IndexedColumn;
struct FieldInfo;
using ObjectPtr = std::shared_ptr<Object>;
using TablePtr = std::shared_ptr<Table>;
using IndexPtr = std::shared_ptr<Index>;
@@ -71,40 +70,24 @@ using ConstraintPtr = std::shared_ptr<Constraint>;
using FieldVector = std::vector<Field>;
using IndexedColumnVector = std::vector<IndexedColumn>;
using ConstraintSet = std::set<ConstraintPtr>;
using FieldInfoList = std::vector<FieldInfo>;
class Object
{
public:
enum Types
{
Table,
Index,
View,
Trigger
};
explicit Object(const std::string& name): m_name(name), m_fullyParsed(false) {}
virtual ~Object() = default;
bool operator==(const Object& rhs) const;
virtual Types type() const = 0;
static std::string typeToString(Types type);
void setName(const std::string& name) { m_name = name; }
const std::string& name() const { return m_name; }
void setOriginalSql(const std::string& original_sql) { m_originalSql = original_sql; }
std::string originalSql() const { return m_originalSql; }
virtual std::string baseTable() const { return std::string(); }
void setFullyParsed(bool fully_parsed) { m_fullyParsed = fully_parsed; }
bool fullyParsed() const { return m_fullyParsed; }
virtual FieldInfoList fieldInformation() const { return FieldInfoList(); }
/**
* @brief Returns the CREATE statement for this object
* @param schema The schema name of the object
@@ -420,18 +403,6 @@ private:
std::shared_ptr<GeneratedColumnConstraint> m_generated;
};
struct FieldInfo
{
FieldInfo(const std::string& name_, const std::string& type_, const std::string& sql_, Field::Affinity affinity_)
: name(name_), type(type_), sql(sql_), affinity(affinity_)
{}
std::string name;
std::string type;
std::string sql;
Field::Affinity affinity;
};
class Table : public Object
{
public:
@@ -441,7 +412,7 @@ public:
bool operator==(const Table& rhs) const;
Types type() const override { return Object::Table; }
virtual bool isView() const { return false; }
FieldVector fields;
using field_type = Field;
@@ -463,8 +434,6 @@ public:
const std::string& virtualUsing() const { return m_virtual; }
bool isVirtual() const { return !m_virtual.empty(); }
FieldInfoList fieldInformation() const override;
void addConstraint(ConstraintPtr constraint);
void setConstraint(ConstraintPtr constraint);
void removeConstraint(ConstraintPtr constraint);
@@ -531,14 +500,10 @@ public:
explicit Index(const std::string& name): Object(name), m_unique(false) {}
Index& operator=(const Index& rhs);
Types type() const override { return Object::Index; }
IndexedColumnVector fields;
using field_type = IndexedColumn;
using field_iterator = IndexedColumnVector::iterator;
std::string baseTable() const override { return m_table; }
void setUnique(bool unique) { m_unique = unique; }
bool unique() const { return m_unique; }
@@ -561,8 +526,6 @@ public:
*/
static IndexPtr parseSQL(const std::string& sSQL);
FieldInfoList fieldInformation() const override;
private:
StringVector columnSqlList() const;
@@ -571,23 +534,17 @@ private:
std::string m_whereExpr;
};
class View : public Object
class View : public Table
{
public:
explicit View(const std::string& name): Object(name) {}
explicit View(const std::string& name): Table(name) {}
Types type() const override { return Object::View; }
FieldVector fields;
virtual bool isView() const override { return true; }
std::string sql(const std::string& /*schema*/ = "main", bool /*ifNotExists*/ = false) const override
{ /* TODO */ return m_originalSql; }
static ViewPtr parseSQL(const std::string& sSQL);
StringVector fieldNames() const;
FieldInfoList fieldInformation() const override;
};
class Trigger : public Object
@@ -595,15 +552,11 @@ class Trigger : public Object
public:
explicit Trigger(const std::string& name): Object(name) {}
Types type() const override { return Object::Trigger; }
std::string sql(const std::string& /*schema*/ = "main", bool /*ifNotExists*/ = false) const override
{ /* TODO */ return m_originalSql; }
static TriggerPtr parseSQL(const std::string& sSQL);
std::string baseTable() const override { return m_table; }
void setTable(const std::string& table) { m_table = table; }
std::string table() const { return m_table; }
@@ -611,6 +564,22 @@ private:
std::string m_table;
};
/**
* @brief Return the name of the base table of the given object. For indices and triggers that is the table which the object is related to.
*/
template<typename T>
std::string getBaseTable(std::shared_ptr<T> /*object*/)
{
return std::string{};
}
template<>
std::string getBaseTable<Index>(IndexPtr object);
template<>
std::string getBaseTable<Trigger>(TriggerPtr object);
/**
* @brief findField Finds a field in the database object and returns an iterator to it.
* @param object
@@ -681,12 +650,11 @@ bool removeField(T& object, const std::string& name)
* TODO Remove this function. Whereever it is used we make the assumption that the queried columns are exactly equal to the columns of the table or view.
* For more complex queries this is not true and in fact it already is a dubious assumption because we also select the rowid column.
*/
inline size_t getFieldNumber(ObjectPtr object, const std::string& name)
inline size_t getFieldNumber(TablePtr object, const std::string& name)
{
const auto object_fields = object->fieldInformation();
for(size_t i=0;i<object_fields.size();i++)
for(size_t i=0;i<object->fields.size();i++)
{
if(object_fields[i].name == name)
if(object->fields[i].name() == name)
return i;
}
return 0;

View File

@@ -879,16 +879,15 @@ bool DBBrowserDB::dump(const QString& filePath,
// Count the total number of all records in all tables for the progress dialog
size_t numRecordsTotal = 0;
objectMap objMap = schemata.at("main"); // We only always export the main database, not the attached databases
std::vector<sqlb::ObjectPtr> tables;
auto all_tables = objMap.equal_range("table");
for(auto it=all_tables.first;it!=all_tables.second;++it)
std::vector<sqlb::TablePtr> tables;
for(const auto& it : objMap.tables)
{
// Never export the sqlite_stat1 and the sqlite_sequence tables if they exist. Also only export any tables which are selected for export.
if(it->second->name() != "sqlite_stat1" && it->second->name() != "sqlite_sequence" && contains(tablesToDump, it->second->name()))
if(!it.second->isView() && it.first != "sqlite_stat1" && it.first != "sqlite_sequence" && contains(tablesToDump, it.first))
{
// Get the number of records in this table and remember to export it
tables.push_back(it->second);
numRecordsTotal += querySingleValueFromDb("SELECT COUNT(*) FROM " + sqlb::ObjectIdentifier("main", it->second->name()).toString()).toUInt();
tables.push_back(it.second);
numRecordsTotal += querySingleValueFromDb("SELECT COUNT(*) FROM " + sqlb::ObjectIdentifier("main", it.first).toString()).toUInt();
}
}
@@ -927,7 +926,7 @@ bool DBBrowserDB::dump(const QString& filePath,
for(const auto& it : tables)
{
// get columns
sqlb::StringVector cols = std::dynamic_pointer_cast<sqlb::Table>(it)->fieldNames();
sqlb::StringVector cols = it->fieldNames();
std::string sQuery = "SELECT * FROM " + sqlb::escapeIdentifier(it->name());
sqlite3_stmt *stmt;
@@ -1014,33 +1013,37 @@ bool DBBrowserDB::dump(const QString& filePath,
// Finally export all objects other than tables
if(exportSchema)
{
for(const auto& obj : objMap)
{
const auto& it = obj.second;
// Make sure it's not a table again
if(it->type() == sqlb::Object::Types::Table)
continue;
// If this object is based on a table (e.g. is an index for that table) it depends on the existence of this table.
// So if we didn't export the base table this depends on, don't export this object either.
if(!it->baseTable().empty() && !contains(tablesToDump, it->baseTable()))
continue;
// Write the SQL string used to create this object to the output file
if(!it->originalSql().empty())
auto writeSchema = [&stream, &tablesToDump, keepOldSchema](const QString& type, auto objects) {
for(const auto& obj : objects)
{
if(!keepOldSchema)
stream << QString("DROP %1 IF EXISTS %2;\n").arg(
QString::fromStdString(sqlb::Object::typeToString(it->type())).toUpper(),
QString::fromStdString(sqlb::escapeIdentifier(it->name())));
const auto& it = obj.second;
if(it->fullyParsed())
stream << QString::fromStdString(it->sql("main", true)) << "\n";
else
stream << QString::fromStdString(it->originalSql()) << ";\n";
// If this object is based on a table (e.g. is an index for that table) it depends on the existence of this table.
// So if we didn't export the base table this depends on, don't export this object either.
if(!sqlb::getBaseTable(it).empty() && !contains(tablesToDump, sqlb::getBaseTable(it)))
continue;
// Write the SQL string used to create this object to the output file
if(!it->originalSql().empty())
{
if(!keepOldSchema)
stream << QString("DROP %1 IF EXISTS %2;\n").arg(
type.toUpper(),
QString::fromStdString(sqlb::escapeIdentifier(it->name())));
if(it->fullyParsed())
stream << QString::fromStdString(it->sql("main", true)) << "\n";
else
stream << QString::fromStdString(it->originalSql()) << ";\n";
}
}
}
};
std::map<std::string, sqlb::TablePtr> views;
std::copy_if(objMap.tables.begin(), objMap.tables.end(), std::inserter(views, views.end()), [](const auto& t) { return t.second->isView(); });
writeSchema("view", views);
writeSchema("index", objMap.indices);
writeSchema("trigger", objMap.triggers);
}
// Done
@@ -1297,7 +1300,7 @@ 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 = getObjectByName<sqlb::Table>(table)->rowidColumns();
sqlb::StringVector pks = getTableByName(table)->rowidColumns();
if(pks.size() == 1)
query += sqlb::escapeIdentifier(pks.front()) + "='" + rowid.toStdString() + "'";
else
@@ -1405,7 +1408,7 @@ QString DBBrowserDB::addRecord(const sqlb::ObjectIdentifier& tablename)
if(!_db)
return QString();
sqlb::TablePtr table = getObjectByName<sqlb::Table>(tablename);
sqlb::TablePtr table = getTableByName(tablename);
if(!table)
return QString();
@@ -1548,7 +1551,7 @@ sqlb::StringVector DBBrowserDB::primaryKeyForEditing(const sqlb::ObjectIdentifie
if(pseudo_pk.empty())
{
sqlb::TablePtr tbl = getObjectByName<sqlb::Table>(table);
sqlb::TablePtr tbl = getTableByName(table);
if(tbl)
return tbl->rowidColumns();
} else {
@@ -1591,14 +1594,14 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb
newSchemaName = tablename.schema();
// When renaming the table in the current schema, check if it doesn't exist already in there
if(tablename.name() != new_table.name() && getObjectByName(sqlb::ObjectIdentifier(newSchemaName, new_table.name())) != nullptr)
if(tablename.name() != new_table.name() && getTableByName(sqlb::ObjectIdentifier(newSchemaName, new_table.name())) != nullptr)
{
lastErrorMessage = tr("A table with the name '%1' already exists in schema '%2'.").arg(QString::fromStdString(new_table.name()), QString::fromStdString(newSchemaName));
return false;
}
} else {
// We're moving the table to a different schema. So check first if it doesn't already exist in the new schema.
if(newSchemaName != tablename.schema() && getObjectByName(sqlb::ObjectIdentifier(newSchemaName, new_table.name())) != nullptr)
if(newSchemaName != tablename.schema() && getTableByName(sqlb::ObjectIdentifier(newSchemaName, new_table.name())) != nullptr)
{
lastErrorMessage = tr("A table with the name '%1' already exists in schema '%2'.").arg(QString::fromStdString(new_table.name()), QString::fromStdString(newSchemaName));
return false;
@@ -1606,7 +1609,7 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb
}
// Get old table schema
sqlb::TablePtr old_table_ptr = getObjectByName<sqlb::Table>(tablename);
sqlb::TablePtr old_table_ptr = getTableByName(tablename);
if(old_table_ptr == nullptr)
{
lastErrorMessage = tr("No table with name '%1' exists in schema '%2'.").arg(QString::fromStdString(tablename.name()), QString::fromStdString(tablename.schema()));
@@ -1735,7 +1738,7 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb
if(changed_something)
{
updateSchema();
old_table = *getObjectByName<sqlb::Table>(sqlb::ObjectIdentifier(tablename.schema(), new_table.name()));
old_table = *getTableByName(sqlb::ObjectIdentifier(tablename.schema(), new_table.name()));
}
// Check if there's still more work to be done or if we are finished now
@@ -1803,53 +1806,58 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb
// Save all indices, triggers and views associated with this table because SQLite deletes them when we drop the table in the next step
std::vector<std::string> otherObjectsSql;
for(const auto& schema : schemata[tablename.schema()])
{
const auto& it = schema.second;
// If this object references the table and it's not the table itself save it's SQL string
if(it->baseTable() == old_table.name() && it->type() != sqlb::Object::Types::Table)
auto saveRelatedObjects = [old_table, track_columns, &otherObjectsSql, newSchemaName](const auto& objects) {
for(const auto& obj : objects)
{
// If this is an index, update the fields first. This highly increases the chance that the SQL statement won't throw an
// error later on when we try to recreate it.
if(it->type() == sqlb::Object::Types::Index)
const auto& it = obj.second;
// If this object references the table save its SQL string
if(sqlb::getBaseTable(it) == old_table.name())
{
sqlb::IndexPtr idx = std::dynamic_pointer_cast<sqlb::Index>(it);
// Loop through all changes to the table schema. For indices only the column names are relevant, so it suffices to look at the
// list of tracked columns
for(const auto& from_it : track_columns)
// If this is an index, update the fields first. This highly increases the chance that the SQL statement won't throw an
// error later on when we try to recreate it.
if(std::is_same<decltype(objects), decltype(objectMap::indices)>::value)
{
const auto& from = from_it.first;
const auto& to = from_it.second;
sqlb::IndexPtr idx = std::dynamic_pointer_cast<sqlb::Index>(it);
// Are we updating the field name or are we removing the field entirely?
if(!to.isNull())
// Loop through all changes to the table schema. For indices only the column names are relevant, so it suffices to look at the
// list of tracked columns
for(const auto& from_it : track_columns)
{
// We're updating the field name. So search for it in the index and replace it wherever it is found
for(size_t i=0;i<idx->fields.size();i++)
{
if(idx->fields[i].name() == from.toStdString())
idx->fields[i].setName(to.toStdString());
}
} else {
// We're removing a field. So remove it from any indices, too.
while(sqlb::removeField(idx, from.toStdString()))
;
}
}
const auto& from = from_it.first;
const auto& to = from_it.second;
// Only try to add the index later if it has any columns remaining. Also use the new schema name here, too, to basically move
// any index that references the table to the same new schema as the table.
if(idx->fields.size())
otherObjectsSql.push_back(idx->sql(newSchemaName));
} else {
// If it's a view or a trigger we don't have any chance to corrections yet. Just store the statement as is and
// hope for the best.
otherObjectsSql.push_back(it->originalSql() + ";");
// Are we updating the field name or are we removing the field entirely?
if(!to.isNull())
{
// We're updating the field name. So search for it in the index and replace it wherever it is found
for(size_t i=0;i<idx->fields.size();i++)
{
if(idx->fields[i].name() == from.toStdString())
idx->fields[i].setName(to.toStdString());
}
} else {
// We're removing a field. So remove it from any indices, too.
while(sqlb::removeField(idx, from.toStdString()))
;
}
}
// Only try to add the index later if it has any columns remaining. Also use the new schema name here, too, to basically move
// any index that references the table to the same new schema as the table.
if(idx->fields.size())
otherObjectsSql.push_back(idx->sql(newSchemaName));
} else {
// If it's a view or a trigger we don't have any chance to corrections yet. Just store the statement as is and
// hope for the best.
otherObjectsSql.push_back(it->originalSql() + ";");
}
}
}
}
};
saveRelatedObjects(schemata[tablename.schema()].tables); // We can safely pass the tables along with the views here since they never have a base table set
saveRelatedObjects(schemata[tablename.schema()].indices);
saveRelatedObjects(schemata[tablename.schema()].triggers);
// We need to disable foreign keys here. The reason is that in the next step the entire table will be dropped and there might be foreign keys
// in other tables that reference this table. These foreign keys would then cause the drop command in the next step to fail. However, we can't
@@ -1964,7 +1972,7 @@ void DBBrowserDB::updateSchema()
const std::string schema_name = db_values.at(1).toStdString();
// Always add the schema to the map. This makes sure it's even then added when there are no objects in the database
schemata[schema_name] = objectMap();
objectMap& object_map = schemata[schema_name];
// Get a list of all the tables for the current database schema. We need to do this differently for normal databases and the temporary schema
// because SQLite doesn't understand the "temp.sqlite_master" notation.
@@ -1974,7 +1982,7 @@ void DBBrowserDB::updateSchema()
else
statement = "SELECT type,name,sql,tbl_name FROM " + sqlb::escapeIdentifier(schema_name) + ".sqlite_master;";
if(!executeSQL(statement, false, true, [this, schema_name](int, std::vector<QByteArray> values, std::vector<QByteArray>) -> bool {
if(!executeSQL(statement, false, true, [this, schema_name, &object_map](int, std::vector<QByteArray> values, std::vector<QByteArray>) -> bool {
const std::string val_type = values.at(0).toStdString();
const std::string val_name = values.at(1).toStdString();
std::string val_sql = values.at(2).toStdString();
@@ -1984,51 +1992,45 @@ void DBBrowserDB::updateSchema()
{
val_sql.erase(std::remove(val_sql.begin(), val_sql.end(), '\r'), val_sql.end());
sqlb::ObjectPtr object;
if(val_type == "table")
object = sqlb::Table::parseSQL(val_sql);
else if(val_type == "index")
object = sqlb::Index::parseSQL(val_sql);
else if(val_type == "trigger")
object = sqlb::Trigger::parseSQL(val_sql);
else if(val_type == "view")
object = sqlb::View::parseSQL(val_sql);
else
return false;
// If parsing wasn't successful set the object name and SQL manually, so that at least the name is going to be correct
if(!object->fullyParsed())
if(val_type == "table" || val_type == "view")
{
object->setName(val_name);
object->setOriginalSql(val_sql);
}
sqlb::TablePtr table;
if(val_type == "table")
table = sqlb::Table::parseSQL(val_sql);
else
table = sqlb::View::parseSQL(val_sql);
// For virtual tables, views, and tables we could not parse at all,
// query the column list using the SQLite pragma to at least get
// some information on them when our parser does not.
if((!object->fullyParsed() && object->fieldInformation().empty()) ||
(object->type() == sqlb::Object::Types::Table && std::dynamic_pointer_cast<sqlb::Table>(object)->isVirtual()) ||
object->type() == sqlb::Object::Types::View)
{
const auto columns = queryColumnInformation(schema_name, val_name);
if(!table->fullyParsed())
table->setName(val_name);
if(object->type() == sqlb::Object::Types::Table)
// For virtual tables, views, and tables we could not parse at all,
// query the column list using the SQLite pragma to at least get
// some information on them when our parser does not.
if((!table->fullyParsed() && table->fields.empty()) || table->isVirtual())
{
sqlb::TablePtr tab = std::dynamic_pointer_cast<sqlb::Table>(object);
for(const auto& column : columns)
tab->fields.emplace_back(column.first, column.second);
} else {
sqlb::ViewPtr view = std::dynamic_pointer_cast<sqlb::View>(object);
for(const auto& column : columns)
view->fields.emplace_back(column.first, column.second);
}
} else if(object->type() == sqlb::Object::Types::Trigger) {
// For triggers set the name of the table the trigger operates on here because we don't have a parser for trigger statements yet.
sqlb::TriggerPtr trg = std::dynamic_pointer_cast<sqlb::Trigger>(object);
trg->setTable(val_tblname);
}
const auto columns = queryColumnInformation(schema_name, val_name);
schemata[schema_name].insert({val_type, object});
for(const auto& column : columns)
table->fields.emplace_back(column.first, column.second);
}
object_map.tables.insert({val_name, table});
} else if(val_type == "index") {
sqlb::IndexPtr index = sqlb::Index::parseSQL(val_sql);
if(!index->fullyParsed())
index->setName(val_name);
object_map.indices.insert({val_name, index});
} else if(val_type == "trigger") {
sqlb::TriggerPtr trigger = sqlb::Trigger::parseSQL(val_sql);
trigger->setName(val_name);
trigger->setOriginalSql(val_sql);
// For triggers set the name of the table the trigger operates on here because we don't have a parser for trigger statements yet.
trigger->setTable(val_tblname);
object_map.triggers.insert({val_name, trigger});
}
}
return false;
@@ -2199,7 +2201,7 @@ std::string DBBrowserDB::generateTemporaryTableName(const std::string& schema) c
while(true)
{
std::string table_name = "sqlb_temp_table_" + std::to_string(++counter);
if(!getObjectByName(sqlb::ObjectIdentifier(schema, table_name)))
if(!getTableByName(sqlb::ObjectIdentifier(schema, table_name)))
return table_name;
}
}

View File

@@ -25,7 +25,19 @@ enum LogMessageType
kLogMsg_ErrorLog
};
using objectMap = std::multimap<std::string, sqlb::ObjectPtr>; // Maps from object type (table, index, view, trigger) to a pointer to the object representation
struct objectMap
{
// These map from object name to object pointer
std::map<std::string, sqlb::TablePtr> tables; // This stores tables AND views
std::map<std::string, sqlb::IndexPtr> indices;
std::map<std::string, sqlb::TriggerPtr> triggers;
bool empty() const
{
return tables.empty() && indices.empty() && triggers.empty();
}
};
using schemaMap = std::map<std::string, objectMap>; // Maps from the schema name (main, temp, attached schemas) to the object map for that schema
int collCompare(void* pArg, int sizeA, const void* sA, int sizeB, const void* sB);
@@ -209,15 +221,28 @@ public:
*/
bool alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb::Table& new_table, AlterTableTrackColumns track_columns, std::string newSchemaName = std::string());
template<typename T = sqlb::Object>
const std::shared_ptr<T> getObjectByName(const sqlb::ObjectIdentifier& name) const
const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const
{
for(auto& it : schemata.at(name.schema()))
{
if(it.second->name() == name.name())
return std::dynamic_pointer_cast<T>(it.second);
}
return std::shared_ptr<T>();
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
{
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
{
const auto& schema = schemata.at(name.schema());
if(schema.triggers.count(name.name()))
return schema.triggers.at(name.name());
return sqlb::TriggerPtr{};
}
bool isOpen() const;

View File

@@ -127,15 +127,14 @@ void SqliteTableModel::setQuery(const sqlb::Query& query)
// Save the query
m_query = query;
// Retrieve field names and types
sqlb::ObjectPtr object = m_db.getObjectByName(query.table());
// Set the row id columns
m_table_of_query = std::dynamic_pointer_cast<sqlb::Table>(object);
if(m_table_of_query) // It is a table
m_table_of_query = m_db.getTableByName(query.table());
if(!m_table_of_query->isView())
{
// It is a table
m_query.setRowIdColumns(m_table_of_query->rowidColumns());
} else {
// It is a view
if(m_query.rowIdColumns().empty())
m_query.setRowIdColumn("_rowid_");
}
@@ -143,10 +142,10 @@ void SqliteTableModel::setQuery(const sqlb::Query& query)
m_headers.push_back(sqlb::joinStringVector(m_query.rowIdColumns(), ","));
// Store field names and affinity data types
for(const auto& fld : object->fieldInformation())
for(const auto& fld : m_table_of_query->fields)
{
m_headers.push_back(fld.name);
m_vDataTypes.push_back(fld.affinity);
m_headers.push_back(fld.name());
m_vDataTypes.push_back(fld.affinity());
}
// Tell the query object about the column names
@@ -960,7 +959,7 @@ bool SqliteTableModel::isEditable(const QModelIndex& index) const
return false;
if(!m_db.isOpen())
return false;
if(!m_table_of_query && !m_query.hasCustomRowIdColumn())
if((!m_table_of_query || m_table_of_query->isView()) && !m_query.hasCustomRowIdColumn())
return false;
// Extra check when the index parameter is set and pointing to a generated column in a table