Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/EditTableDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ EditTableDialog::EditTableDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier&
ui->treeWidget->setItemDelegateForColumn(kAutoIncrement, new NoEditDelegate(this));
ui->treeWidget->setItemDelegateForColumn(kUnique, new NoEditDelegate(this));
ui->treeWidget->setItemDelegateForColumn(kCollation, new NoEditDelegate(this));
ui->treeWidget->setItemDelegateForColumn(kStored, new NoEditDelegate(this));

// Set up popup menu for adding constraints
QMenu* constraint_menu = new QMenu(this);
Expand Down Expand Up @@ -177,6 +178,8 @@ void EditTableDialog::updateColumnWidth()
ui->treeWidget->setColumnWidth(kPrimaryKey, 25);
ui->treeWidget->setColumnWidth(kAutoIncrement, 25);
ui->treeWidget->setColumnWidth(kUnique, 25);
ui->treeWidget->setColumnWidth(kGenerated, 150);
ui->treeWidget->setColumnWidth(kStored, 50);
ui->treeWidget->setColumnWidth(kForeignKey, 500);

ui->tableIndexConstraints->setColumnWidth(kConstraintColumns, 180);
Expand Down Expand Up @@ -255,6 +258,11 @@ void EditTableDialog::populateFields()
connect(collationBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTypeAndCollation()));
ui->treeWidget->setItemWidget(tbitem, kCollation, collationBox);

// Generated columns show their expression. The checkbox tells whether they are stored instead of virtual.
if(f.generated())
tbitem->setText(kGenerated, QString::fromStdString(f.generated()->expression()));
tbitem->setCheckState(kStored, f.generated() && f.generated()->storage() == "STORED" ? Qt::Checked : Qt::Unchecked);

auto fk = m_table.foreignKey({f.name()});
if(fk)
tbitem->setText(kForeignKey, QString::fromStdString(fk->toString()));
Expand Down Expand Up @@ -621,6 +629,17 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
break;
case kPrimaryKey:
{
// A generated column cannot be part of the primary key
if(item->checkState(column) == Qt::Checked && field.generated())
{
QMessageBox::warning(this, qApp->applicationName(), tr("A generated column cannot be part of the primary key."));

ui->treeWidget->blockSignals(true);
item->setCheckState(column, Qt::Unchecked);
ui->treeWidget->blockSignals(false);
return;
}

// Check if there already is a primary key
auto pk = m_table.primaryKey();
if(pk)
Expand Down Expand Up @@ -686,6 +705,16 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
case kAutoIncrement:
{
bool ischecked = item->checkState(column) == Qt::Checked;
if(ischecked && field.generated())
{
// Autoincrement requires a primary key
QMessageBox::warning(this, qApp->applicationName(), tr("A generated column cannot be part of the primary key."));

ui->treeWidget->blockSignals(true);
item->setCheckState(column, Qt::Unchecked);
ui->treeWidget->blockSignals(false);
return;
}
if(ischecked)
{
// First check if the contents of this column are all integers. If not this field cannot be set to AI
Expand Down Expand Up @@ -775,6 +804,18 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
case kDefault:
{
QString new_value = item->text(column);

// A generated column cannot have a default value
if(!new_value.isEmpty() && field.generated())
{
QMessageBox::warning(this, qApp->applicationName(), tr("A generated column cannot have a default value."));

ui->treeWidget->blockSignals(true);
item->setText(column, QString());
ui->treeWidget->blockSignals(false);
return;
}

// If the default value isn't a SQL keyword perform an extra check: If it isn't numeric but doesn't start and end with quotes,
// add the quotes
if(new_value.size() && new_value.compare("null", Qt::CaseInsensitive) &&
Expand Down Expand Up @@ -812,6 +853,76 @@ void EditTableDialog::fieldItemChanged(QTreeWidgetItem *item, int column)
case kCheck:
field.setCheck(item->text(column).toStdString());
break;
case kGenerated:
{
const std::string expression = item->text(column).trimmed().toStdString();
if(expression.empty())
{
// No expression, so this is an ordinary column
field.setGenerated(nullptr);

ui->treeWidget->blockSignals(true);
item->setCheckState(kStored, Qt::Unchecked);
ui->treeWidget->blockSignals(false);
} else {
// Checks whether the expression can be applied. If not, the old expression is restored in the view.
QString error;
const auto pkColumns = m_table.primaryKeyColumns();
if(contains(pkColumns, field.name()))
error = tr("A generated column cannot be part of the primary key. Please remove the primary key flag first.");
else if(!field.defaultValue().empty())
error = tr("A generated column cannot have a default value. Please remove the default value first.");
if(!error.isEmpty())
{
QMessageBox::warning(this, qApp->applicationName(), error);

ui->treeWidget->blockSignals(true);
item->setText(column, field.generated() ? QString::fromStdString(field.generated()->expression()) : QString());
ui->treeWidget->blockSignals(false);
return;
}

// Turning an existing column into a generated column replaces its contents by the calculated values
if(!m_bNewTable && !field.generated())
{
const sqlb::TablePtr old_table = pdb.getTableByName(curTable);
bool has_data = false;
for(const auto& it : trackColumns)
{
if(!it.first.isNull() && it.second == QString::fromStdString(field.name()))
{
auto old_field = sqlb::findField(*old_table, it.first.toStdString());
has_data = old_field != old_table->fields.end() && !old_field->generated();
}
}
if(has_data && QMessageBox::warning(this, QApplication::applicationName(),
tr("Are you sure you want to make the field '%1' a generated column?\nAll data currently stored in this field will be replaced by the calculated values.").arg(item->text(kName)),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No)
{
ui->treeWidget->blockSignals(true);
item->setText(column, QString());
ui->treeWidget->blockSignals(false);
return;
}
}

// Always use a new constraint object because the old one might still be used by the table object of the main window
auto generated = std::make_shared<sqlb::GeneratedColumnConstraint>(expression, item->checkState(kStored) == Qt::Checked ? "STORED" : "VIRTUAL");
if(field.generated())
generated->setName(field.generated()->name());
field.setGenerated(generated);
}
}
break;
case kStored:
// Without an expression there is nothing to change here yet. The check state is applied as soon as an expression is entered.
if(field.generated())
{
auto generated = std::make_shared<sqlb::GeneratedColumnConstraint>(field.generated()->expression(), item->checkState(column) == Qt::Checked ? "STORED" : "VIRTUAL");
generated->setName(field.generated()->name());
field.setGenerated(generated);
}
break;
case kForeignKey:
// handled in delegate
break;
Expand Down Expand Up @@ -1002,6 +1113,7 @@ void EditTableDialog::addField()
tbitem->setCheckState(kPrimaryKey, Qt::Unchecked);
tbitem->setCheckState(kAutoIncrement, Qt::Unchecked);
tbitem->setCheckState(kUnique, Qt::Unchecked);
tbitem->setCheckState(kStored, Qt::Unchecked);

ui->treeWidget->addTopLevelItem(tbitem);
ui->treeWidget->scrollToBottom();
Expand Down
4 changes: 3 additions & 1 deletion src/EditTableDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class EditTableDialog : public QDialog
kDefault = 6,
kCheck = 7,
kCollation = 8,
kForeignKey = 9
kGenerated = 9,
kStored = 10,
kForeignKey = 11
};

enum IndexConstraintColumns {
Expand Down
16 changes: 16 additions & 0 deletions src/EditTableDialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,22 @@
<string>Collation</string>
</property>
</column>
<column>
<property name="text">
<string>Generated</string>
</property>
<property name="toolTip">
<string>Expression of a generated column. Leave empty for an ordinary column.</string>
</property>
</column>
<column>
<property name="text">
<string>Stored</string>
</property>
<property name="toolTip">
<string>Store the value of a generated column in the table instead of calculating it when it is read</string>
</property>
</column>
<column>
<property name="text">
<string>Foreign Key</string>
Expand Down
8 changes: 8 additions & 0 deletions src/sql/sqlitetypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ bool Field::operator==(const Field& rhs) const
if(m_collation != rhs.m_collation)
return false;

// The generated column constraint is compared by value because the Edit Table dialog replaces it by a new object when it is changed
if(static_cast<bool>(m_generated) != static_cast<bool>(rhs.m_generated))
return false;
if(m_generated && (m_generated->expression() != rhs.m_generated->expression() ||
m_generated->storage() != rhs.m_generated->storage() ||
m_generated->name() != rhs.m_generated->name()))
return false;

return true;
}

Expand Down
4 changes: 3 additions & 1 deletion src/sqlitedb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1786,7 +1786,9 @@ bool DBBrowserDB::alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb
return p.first.isNull() && p.second.toStdString() == field.name();
}))
{
if(!addColumn(sqlb::ObjectIdentifier(tablename.schema(), new_table.name()), field))
// SQLite can't add a STORED generated column to an existing table. So skip it here and let part 3 add it by rebuilding the table.
const bool is_stored_column = field.generated() && field.generated()->storage() == "STORED";
if(!is_stored_column && !addColumn(sqlb::ObjectIdentifier(tablename.schema(), new_table.name()), field))
{
revertToSavepoint(savepointName);
return false;
Expand Down
56 changes: 55 additions & 1 deletion src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

find_package(${QT_MAJOR} REQUIRED COMPONENTS Test Widgets)
find_package(${QT_MAJOR} REQUIRED COMPONENTS Concurrent PrintSupport Test Widgets)
if(QT_MAJOR STREQUAL "Qt6")
find_package(Qt6 REQUIRED COMPONENTS Core5Compat)
set(QT5_COMPAT Qt6::Core5Compat)
Expand Down Expand Up @@ -123,3 +123,57 @@ set(TESTCACHE_HDR
add_executable(test-cache ${TESTCACHE_HDR} ${TESTCACHE_SRC})
target_link_libraries(test-cache ${QT_MAJOR}::Test)
add_test(NAME test-cache COMMAND $<TARGET_FILE:test-cache>)

# test edit table dialog

set(TESTEDITTABLE_SRC
TestEditTableDialog.cpp
../EditTableDialog.cpp
../ForeignKeyEditorDelegate.cpp
../SelectItemsPopup.cpp
../sqlitedb.cpp
../sqlitetablemodel.cpp
../RowLoader.cpp
../Settings.cpp
../CondFormat.cpp
../Data.cpp
../CipherDialog.cpp
../CipherSettings.cpp
../sqltextedit.cpp
../SqlUiLexer.cpp
../ExtendedScintilla.cpp
../FindReplaceDialog.cpp
../sql/sqlitetypes.cpp
../sql/Query.cpp
../sql/ObjectIdentifier.cpp
../sql/parser/ParserDriver.cpp
../sql/parser/sqlite3_lexer.cpp
../sql/parser/sqlite3_parser.cpp
)

set(TESTEDITTABLE_HDR
TestEditTableDialog.h
../EditTableDialog.h
../ForeignKeyEditorDelegate.h
../SelectItemsPopup.h
../sqlitedb.h
../sqlitetablemodel.h
../RowLoader.h
../Settings.h
../CondFormat.h
../Data.h
../CipherDialog.h
)

add_executable(test-edit-table ${TESTEDITTABLE_HDR} ${TESTEDITTABLE_SRC} ../CipherDialog.ui)
target_include_directories(test-edit-table PRIVATE "${PROJECT_BINARY_DIR}")
target_link_libraries(test-edit-table ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT_MAJOR}::Concurrent ${QT_MAJOR}::PrintSupport QScintilla::QScintilla ${LIBSQLITE_NAME} ${QT5_COMPAT})
if(QT_MAJOR STREQUAL "Qt6")
# Same as for the main target: the .ui files connect to slots that only exist in Qt5 unless connections are by string
set_target_properties(test-edit-table PROPERTIES AUTOUIC_OPTIONS "--connections=string")
endif()
add_test(NAME test-edit-table COMMAND $<TARGET_FILE:test-edit-table>)
if(WIN32)
# Like test-global-filter, this needs to find the SQLite DLL built by the CI jobs
set_tests_properties(test-edit-table PROPERTIES ENVIRONMENT "PATH=${TESTGLOBALFILTER_PATH}")
endif()
Loading
Loading