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
19 changes: 18 additions & 1 deletion src/sqlitetablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,26 @@ bool SqliteTableModel::setTypedData(const QModelIndex& index, bool isBlob, const

if(index.isValid() && role == Qt::EditRole)
{
// The cache is sparse: rows beyond the chunk loaded so far are valid model rows but not cached yet (e.g. when pasting a large
// clipboard). Load the row first, like find() does, instead of letting at() throw.
const size_t row = static_cast<size_t>(index.row());
bool cached;
{
std::unique_lock<std::mutex> lock(m_mutexDataCache);
cached = m_cache.count(row);
}
if(!cached)
{
triggerCacheLoad(index.row());
waitUntilIdle();
}

std::unique_lock<std::mutex> lock(m_mutexDataCache);

auto & cached_row = m_cache.at(static_cast<size_t>(index.row()));
if(!m_cache.count(row))
return false;

auto & cached_row = m_cache.at(row);
const size_t column = static_cast<size_t>(index.column());

QByteArray newValue = encode(value.toByteArray());
Expand Down
36 changes: 36 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,39 @@ 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 table model

set(TESTTABLEMODEL_SRC
TestTableModel.cpp
../sqlitetablemodel.cpp
../RowLoader.cpp
../sqlitedb.cpp
../CipherSettings.cpp
../CipherDialog.cpp
../CondFormat.cpp
../Data.cpp
../Settings.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(TESTTABLEMODEL_HDR
TestTableModel.h
)

add_executable(test-table-model ${TESTTABLEMODEL_HDR} ${TESTTABLEMODEL_SRC})
target_link_libraries(test-table-model ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT_MAJOR}::Concurrent ${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-table-model PROPERTIES AUTOUIC_OPTIONS "--connections=string")
endif()
add_test(NAME test-table-model COMMAND $<TARGET_FILE:test-table-model>)
if(WIN32)
# Like test-global-filter, this needs to find the SQLite DLL built by the CI jobs
set_tests_properties(test-table-model PROPERTIES ENVIRONMENT "PATH=${TESTGLOBALFILTER_PATH}")
endif()
81 changes: 81 additions & 0 deletions src/tests/TestTableModel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QTemporaryDir>
#include <QFile>

#include "TestTableModel.h"
#include "../sqlitedb.h"
#include "../sqlitetablemodel.h"
#include "../Settings.h"
#include "../sql/ObjectIdentifier.h"

namespace
{
const int rowCount = 100;

// Column 0 of the model is the rowid column, so the 'val' column is column 2.
// Creates a table with rowCount rows and returns a model showing it. The prefetch size is set small enough that
// most rows are not loaded into the cache when the model is created.
struct Fixture
{
QTemporaryDir dir;
DBBrowserDB db;
std::unique_ptr<SqliteTableModel> model;

Fixture()
{
Settings::setValue("db", "prefetchsize", 10);
// Open an empty file instead of using create(), which closes the database handle twice
const QString filename = dir.filePath("test.db");
QFile(filename).open(QIODevice::WriteOnly);
db.open(filename);
db.executeSQL("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);");
db.executeSQL("WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x < " + std::to_string(rowCount) +
") INSERT INTO t(id, val) SELECT x, 'v' || x FROM c;");
model = std::make_unique<SqliteTableModel>(db, nullptr, QString(), true);
model->setQuery(sqlb::Query(sqlb::ObjectIdentifier("main", "t")));
model->waitUntilIdle();
// The worker reports back through queued connections
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount() == rowCount, 5000);
model->waitUntilIdle();
QCoreApplication::processEvents();
}
};
}

void TestTableModel::setDataOnCachedRow()
{
Fixture f;
QCOMPARE(f.model->rowCount(), rowCount);

const QModelIndex idx = f.model->index(0, 2);
QVERIFY(f.model->setData(idx, "changed"));
QCOMPARE(f.model->data(idx, Qt::EditRole).toString(), QString("changed"));
}

void TestTableModel::setDataOnUncachedRowLoadsRowAndUpdates()
{
Fixture f;
QCOMPARE(f.model->rowCount(), rowCount);

// Rows beyond the first chunk are valid model rows, but not in the cache yet. Editing them (e.g. by pasting a
// large clipboard) used to throw std::out_of_range from the cache lookup and terminate the application.
const QModelIndex idx = f.model->index(rowCount - 1, 2);
try {
QVERIFY(f.model->setData(idx, "changed"));
} catch(const std::exception& e) {
QFAIL(qPrintable(QString("setData threw: ") + e.what()));
}
QCOMPARE(f.model->data(idx, Qt::EditRole).toString(), QString("changed"));
}

int main(int argc, char* argv[])
{
// The model needs a QApplication, but the tests must also run on machines without a display (e.g. CI)
if(qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", "offscreen");

QApplication app(argc, argv);
TestTableModel test;
return QTest::qExec(&test, argc, argv);
}
15 changes: 15 additions & 0 deletions src/tests/TestTableModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef TESTTABLEMODEL_H
#define TESTTABLEMODEL_H

#include <QObject>

class TestTableModel : public QObject
{
Q_OBJECT

private slots:
void setDataOnCachedRow();
void setDataOnUncachedRowLoadsRowAndUpdates();
};

#endif // TESTTABLEMODEL_H
Loading