diff --git a/src/libs/sqlite/constraints.h b/src/libs/sqlite/constraints.h index dca7fa5e845..d60a767a280 100644 --- a/src/libs/sqlite/constraints.h +++ b/src/libs/sqlite/constraints.h @@ -38,9 +38,14 @@ class Unique friend bool operator==(Unique, Unique) { return true; } }; +enum class AutoIncrement { No, Yes }; + class PrimaryKey { friend bool operator==(PrimaryKey, PrimaryKey) { return true; } + +public: + AutoIncrement autoincrement = AutoIncrement::No; }; class NotNull diff --git a/src/libs/sqlite/createtablesqlstatementbuilder.cpp b/src/libs/sqlite/createtablesqlstatementbuilder.cpp index 025e0f0e071..2a14fe9ca57 100644 --- a/src/libs/sqlite/createtablesqlstatementbuilder.cpp +++ b/src/libs/sqlite/createtablesqlstatementbuilder.cpp @@ -125,7 +125,12 @@ public: void operator()(const Unique &) { columnDefinitionString.append(" UNIQUE"); } - void operator()(const PrimaryKey &) { columnDefinitionString.append(" PRIMARY KEY"); } + void operator()(const PrimaryKey &primaryKey) + { + columnDefinitionString.append(" PRIMARY KEY"); + if (primaryKey.autoincrement == AutoIncrement::Yes) + columnDefinitionString.append(" AUTOINCREMENT"); + } void operator()(const ForeignKey &foreignKey) { diff --git a/src/libs/sqlite/sqlite-lib.pri b/src/libs/sqlite/sqlite-lib.pri index dcf72e4f3df..4c0b3a18e75 100644 --- a/src/libs/sqlite/sqlite-lib.pri +++ b/src/libs/sqlite/sqlite-lib.pri @@ -17,6 +17,8 @@ SOURCES += \ $$PWD/sqliteglobal.cpp \ $$PWD/sqlitereadstatement.cpp \ $$PWD/sqlitereadwritestatement.cpp \ + $$PWD/sqlitesessionchangeset.cpp \ + $$PWD/sqlitesessions.cpp \ $$PWD/sqlitewritestatement.cpp \ $$PWD/sqlstatementbuilder.cpp \ $$PWD/utf8string.cpp \ @@ -33,6 +35,8 @@ HEADERS += \ $$PWD/sqliteglobal.h \ $$PWD/sqlitereadstatement.h \ $$PWD/sqlitereadwritestatement.h \ + $$PWD/sqlitesessionchangeset.h \ + $$PWD/sqlitesessions.h \ $$PWD/sqlitetransaction.h \ $$PWD/sqlitevalue.h \ $$PWD/sqlitewritestatement.h \ @@ -46,16 +50,16 @@ HEADERS += \ $$PWD/sqliteindex.h \ $$PWD/sqlitebasestatement.h -DEFINES += SQLITE_THREADSAFE=2 SQLITE_ENABLE_FTS5 \ - SQLITE_ENABLE_UNLOCK_NOTIFY SQLITE_ENABLE_JSON1 \ - SQLITE_DEFAULT_FOREIGN_KEYS=1 SQLITE_TEMP_STORE=2 SQLITE_DEFAULT_PAGE_SIZE=32768 \ +DEFINES += SQLITE_THREADSAFE=2 SQLITE_ENABLE_FTS5 SQLITE_ENABLE_UNLOCK_NOTIFY \ + SQLITE_ENABLE_JSON1 SQLITE_DEFAULT_FOREIGN_KEYS=1 SQLITE_TEMP_STORE=2 \ SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 SQLITE_MAX_WORKER_THREADS SQLITE_DEFAULT_MEMSTATUS=0 \ SQLITE_OMIT_DEPRECATED SQLITE_OMIT_DECLTYPE \ SQLITE_MAX_EXPR_DEPTH=0 SQLITE_OMIT_SHARED_CACHE SQLITE_USE_ALLOCA \ SQLITE_ENABLE_MEMORY_MANAGEMENT SQLITE_ENABLE_NULL_TRIM SQLITE_OMIT_EXPLAIN \ SQLITE_OMIT_LOAD_EXTENSION SQLITE_OMIT_UTF16 SQLITE_DQS=0 \ SQLITE_ENABLE_STAT4 HAVE_ISNAN HAVE_FDATASYNC HAVE_MALLOC_USABLE_SIZE \ - SQLITE_DEFAULT_MMAP_SIZE=268435456 SQLITE_CORE + SQLITE_DEFAULT_MMAP_SIZE=268435456 SQLITE_CORE SQLITE_ENABLE_SESSION SQLITE_ENABLE_PREUPDATE_HOOK \ + SQLITE_LIKE_DOESNT_MATCH_BLOBS CONFIG(debug, debug|release): DEFINES += SQLITE_ENABLE_API_ARMOR diff --git a/src/libs/sqlite/sqlite.pro b/src/libs/sqlite/sqlite.pro index f3c75fa3731..9b2b30cf08e 100644 --- a/src/libs/sqlite/sqlite.pro +++ b/src/libs/sqlite/sqlite.pro @@ -3,7 +3,5 @@ win32:QMAKE_CXXFLAGS_DEBUG += -O2 include(../../qtcreatorlibrary.pri) -win32:DEFINES += SQLITE_API=__declspec(dllexport) -unix:DEFINES += SQLITE_API=\"__attribute__((visibility(\\\"default\\\")))\" include(sqlite-lib.pri) diff --git a/src/libs/sqlite/sqlitecolumn.h b/src/libs/sqlite/sqlitecolumn.h index ef7cb26c4ac..1775a683075 100644 --- a/src/libs/sqlite/sqlitecolumn.h +++ b/src/libs/sqlite/sqlitecolumn.h @@ -67,6 +67,8 @@ public: return "REAL"; case ColumnType::Text: return "TEXT"; + case ColumnType::Blob: + return "BLOB"; } Q_UNREACHABLE(); diff --git a/src/libs/sqlite/sqlitedatabase.cpp b/src/libs/sqlite/sqlitedatabase.cpp index 688801c02e1..084fd3dd1e0 100644 --- a/src/libs/sqlite/sqlitedatabase.cpp +++ b/src/libs/sqlite/sqlitedatabase.cpp @@ -25,9 +25,10 @@ #include "sqlitedatabase.h" +#include "sqlitereadwritestatement.h" +#include "sqlitesessions.h" #include "sqlitetable.h" #include "sqlitetransaction.h" -#include "sqlitereadwritestatement.h" #include @@ -51,6 +52,7 @@ public: ReadWriteStatement exclusiveBegin{"BEGIN EXCLUSIVE", database}; ReadWriteStatement commitBegin{"COMMIT", database}; ReadWriteStatement rollbackBegin{"ROLLBACK", database}; + Sessions sessions{database, "main", "databaseSessions"}; }; Database::Database() @@ -60,17 +62,20 @@ Database::Database() Database::Database(Utils::PathString &&databaseFilePath, JournalMode journalMode) : Database(std::move(databaseFilePath), 1000ms, journalMode) -{ -} +{} Database::Database(Utils::PathString &&databaseFilePath, std::chrono::milliseconds busyTimeout, JournalMode journalMode) - : m_databaseBackend(*this), - m_busyTimeout(busyTimeout) + : m_databaseBackend(*this) + , m_busyTimeout(busyTimeout) { setJournalMode(journalMode); open(std::move(databaseFilePath)); + +#ifndef QT_NO_DEBUG + execute("PRAGMA reverse_unordered_selects=1"); +#endif } Database::~Database() = default; @@ -136,6 +141,16 @@ void Database::setDatabaseFilePath(Utils::PathString &&databaseFilePath) m_databaseFilePath = std::move(databaseFilePath); } +void Database::setAttachedTables(const Utils::SmallStringVector &tables) +{ + m_statements->sessions.setAttachedTables(tables); +} + +void Database::applyAndUpdateSessions() +{ + m_statements->sessions.applyAndUpdateSessions(); +} + const Utils::PathString &Database::databaseFilePath() const { return m_databaseFilePath; @@ -215,6 +230,22 @@ void Database::rollback() m_statements->rollbackBegin.execute(); } +void Database::immediateSessionBegin() +{ + m_statements->immediateBegin.execute(); + m_statements->sessions.create(); +} +void Database::sessionCommit() +{ + m_statements->sessions.commit(); + m_statements->commitBegin.execute(); +} +void Database::sessionRollback() +{ + m_statements->sessions.rollback(); + m_statements->rollbackBegin.execute(); +} + void Database::lock() { m_databaseMutex.lock(); diff --git a/src/libs/sqlite/sqlitedatabase.h b/src/libs/sqlite/sqlitedatabase.h index 31de1d7b015..1fd8e832ef1 100644 --- a/src/libs/sqlite/sqlitedatabase.h +++ b/src/libs/sqlite/sqlitedatabase.h @@ -123,6 +123,9 @@ public: void resetUpdateHook() { m_databaseBackend.resetUpdateHook(); } + void setAttachedTables(const Utils::SmallStringVector &tables); + void applyAndUpdateSessions(); + private: void deferredBegin() override; void immediateBegin() override; @@ -131,6 +134,9 @@ private: void rollback() override; void lock() override; void unlock() override; + void immediateSessionBegin() override; + void sessionCommit() override; + void sessionRollback() override; void initializeTables(); void registerTransactionStatements(); diff --git a/src/libs/sqlite/sqlitedatabaseinterface.h b/src/libs/sqlite/sqlitedatabaseinterface.h index 1c2a588b7ad..46bc2dcab7d 100644 --- a/src/libs/sqlite/sqlitedatabaseinterface.h +++ b/src/libs/sqlite/sqlitedatabaseinterface.h @@ -25,7 +25,7 @@ #pragma once -#include +#include #include "sqliteglobal.h" @@ -42,6 +42,8 @@ public: virtual void execute(Utils::SmallStringView sqlStatement) = 0; virtual void setUpdateHook(UpdateCallback &callback) = 0; virtual void resetUpdateHook() = 0; + virtual void applyAndUpdateSessions() = 0; + virtual void setAttachedTables(const Utils::SmallStringVector &tables) = 0; protected: ~DatabaseInterface() = default; diff --git a/src/libs/sqlite/sqliteexception.h b/src/libs/sqlite/sqliteexception.h index 7f8f60ca423..bd4245934c8 100644 --- a/src/libs/sqlite/sqliteexception.h +++ b/src/libs/sqlite/sqliteexception.h @@ -43,7 +43,7 @@ public: , m_sqliteErrorMessage(std::move(sqliteErrorMessage)) {} - const char *what() const noexcept override { return m_sqliteErrorMessage.data(); } + const char *what() const noexcept override { return m_whatErrorHasHappen; } void printWarning() const; @@ -283,4 +283,20 @@ public: {} }; +class CannotApplyChangeSet : public Exception +{ +public: + CannotApplyChangeSet(const char *whatErrorHasHappen) + : Exception(whatErrorHasHappen) + {} +}; + +class ChangeSetIsMisused : public Exception +{ +public: + ChangeSetIsMisused(const char *whatErrorHasHappen) + : Exception(whatErrorHasHappen) + {} +}; + } // namespace Sqlite diff --git a/src/libs/sqlite/sqliteglobal.h b/src/libs/sqlite/sqliteglobal.h index 910cf78231c..308bc07fe03 100644 --- a/src/libs/sqlite/sqliteglobal.h +++ b/src/libs/sqlite/sqliteglobal.h @@ -39,14 +39,7 @@ namespace Sqlite { -enum class ColumnType : char -{ - Numeric, - Integer, - Real, - Text, - None -}; +enum class ColumnType : char { Numeric, Integer, Real, Text, Blob, None }; enum class ConstraintType : char { NoConstraint, PrimaryKey, Unique, ForeignKey }; diff --git a/src/libs/sqlite/sqlitesessionchangeset.cpp b/src/libs/sqlite/sqlitesessionchangeset.cpp new file mode 100644 index 00000000000..3aa43c625f0 --- /dev/null +++ b/src/libs/sqlite/sqlitesessionchangeset.cpp @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "sqlitesessionchangeset.h" +#include "sqlitesessions.h" + +#include + +#include + +namespace Sqlite { + +namespace { +void checkResultCode(int resultCode) +{ + switch (resultCode) { + case SQLITE_NOMEM: + throw std::bad_alloc(); + } + + if (resultCode != SQLITE_OK) + throw UnknowError("Unknow exception"); +} + +} // namespace + +SessionChangeSet::SessionChangeSet(Utils::span blob) + : data(sqlite3_malloc64(blob.size())) + , size(int(blob.size())) +{ + std::memcpy(data, blob.data(), blob.size()); +} + +SessionChangeSet::SessionChangeSet(Sessions &session) +{ + int resultCode = sqlite3session_changeset(session.session.get(), &size, &data); + checkResultCode(resultCode); +} + +SessionChangeSet::~SessionChangeSet() +{ + sqlite3_free(data); +} + +Utils::span SessionChangeSet::asSpan() const +{ + return {static_cast(data), static_cast(size)}; +} + +} // namespace Sqlite diff --git a/src/libs/sqlite/sqlitesessionchangeset.h b/src/libs/sqlite/sqlitesessionchangeset.h new file mode 100644 index 00000000000..65396622a92 --- /dev/null +++ b/src/libs/sqlite/sqlitesessionchangeset.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "sqliteglobal.h" + +#include + +#include +#include + +#include + +namespace Sqlite { + +class Sessions; + +class SessionChangeSet +{ +public: + SessionChangeSet(Utils::span blob); + SessionChangeSet(Sessions &session); + ~SessionChangeSet(); + SessionChangeSet(const SessionChangeSet &) = delete; + void operator=(const SessionChangeSet &) = delete; + SessionChangeSet(SessionChangeSet &&other) noexcept + { + SessionChangeSet temp; + swap(temp, other); + swap(temp, *this); + } + void operator=(SessionChangeSet &); + + Utils::span asSpan() const; + + friend void swap(SessionChangeSet &first, SessionChangeSet &second) noexcept + { + SessionChangeSet temp; + std::swap(temp.data, first.data); + std::swap(temp.size, first.size); + std::swap(first.data, second.data); + std::swap(first.size, second.size); + std::swap(temp.data, second.data); + std::swap(temp.size, second.size); + } + +private: + SessionChangeSet() = default; + +public: + void *data = nullptr; + int size = {}; +}; + +using SessionChangeSets = std::vector; + +} // namespace Sqlite diff --git a/src/libs/sqlite/sqlitesessions.cpp b/src/libs/sqlite/sqlitesessions.cpp new file mode 100644 index 00000000000..e377ef9b72d --- /dev/null +++ b/src/libs/sqlite/sqlitesessions.cpp @@ -0,0 +1,188 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "sqlitesessions.h" +#include "sqlitereadstatement.h" +#include "sqlitesessionchangeset.h" +#include "sqlitetable.h" + +#include + +#include + +namespace Sqlite { + +namespace { + +void checkResultCode(int resultCode) +{ + switch (resultCode) { + case SQLITE_NOMEM: + throw std::bad_alloc(); + case SQLITE_SCHEMA: + throw CannotApplyChangeSet("Cannot apply change set!"); + case SQLITE_MISUSE: + throw ChangeSetIsMisused("Change set is misused!"); + } + + if (resultCode != SQLITE_OK) + throw UnknowError("Unknow exception"); +} + +int xConflict(void *, int conflict, sqlite3_changeset_iter *) +{ + switch (conflict) { + case SQLITE_CHANGESET_DATA: + return SQLITE_CHANGESET_REPLACE; + case SQLITE_CHANGESET_NOTFOUND: + return SQLITE_CHANGESET_OMIT; + case SQLITE_CHANGESET_CONFLICT: + return SQLITE_CHANGESET_REPLACE; + case SQLITE_CHANGESET_CONSTRAINT: + return SQLITE_CHANGESET_OMIT; + case SQLITE_CHANGESET_FOREIGN_KEY: + return SQLITE_CHANGESET_OMIT; + } + + return SQLITE_CHANGESET_ABORT; +} +} // namespace + +void Sessions::attachTables(const Utils::SmallStringVector &tableNames) +{ + for (Utils::SmallStringView tableName : tableNames) { + int resultCode = sqlite3session_attach(session.get(), tableName.data()); + checkResultCode(resultCode); + } +} + +Sessions::~Sessions() = default; + +void Sessions::setAttachedTables(Utils::SmallStringVector tables) +{ + tableNames = std::move(tables); +} + +void Sessions::create() +{ + sqlite3_session *newSession = nullptr; + int resultCode = sqlite3session_create(database.backend().sqliteDatabaseHandle(), + databaseName.data(), + &newSession); + session.reset(newSession); + + checkResultCode(resultCode); + + attachTables(tableNames); +} + +void Sessions::commit() +{ + if (session && !sqlite3session_isempty(session.get())) { + SessionChangeSet changeSet{*this}; + + insertSession.write(changeSet.asSpan()); + } + + session.reset(); +} + +void Sessions::rollback() +{ + session.reset(); +} + +void Internal::SessionsBase::createSessionTable(Database &database) +{ + Sqlite::Table table; + table.setUseIfNotExists(true); + table.setName(sessionsTableName); + table.addColumn("id", Sqlite::ColumnType::Integer, {Sqlite::PrimaryKey{AutoIncrement::Yes}}); + table.addColumn("changeset", Sqlite::ColumnType::Blob); + + table.initialize(database); +} + +void Sessions::revert() +{ + ReadStatement selectChangeSets{Utils::PathString{"SELECT changeset FROM ", + sessionsTableName, + " ORDER BY id DESC"}, + database}; + + auto changeSets = selectChangeSets.values(1024); + + for (auto &changeSet : changeSets) { + int resultCode = sqlite3changeset_apply_v2(database.backend().sqliteDatabaseHandle(), + changeSet.size, + changeSet.data, + nullptr, + xConflict, + nullptr, + nullptr, + nullptr, + SQLITE_CHANGESETAPPLY_INVERT + | SQLITE_CHANGESETAPPLY_NOSAVEPOINT); + checkResultCode(resultCode); + } +} + +void Sessions::apply() +{ + ReadStatement selectChangeSets{Utils::PathString{"SELECT changeset FROM ", + sessionsTableName, + " ORDER BY id"}, + database}; + + auto changeSets = selectChangeSets.values(1024); + + for (auto &changeSet : changeSets) { + int resultCode = sqlite3changeset_apply_v2(database.backend().sqliteDatabaseHandle(), + changeSet.size, + changeSet.data, + nullptr, + xConflict, + nullptr, + nullptr, + nullptr, + SQLITE_CHANGESETAPPLY_NOSAVEPOINT); + checkResultCode(resultCode); + } +} + +void Sessions::applyAndUpdateSessions() +{ + create(); + apply(); + deleteAll(); + commit(); +} + +void Sessions::deleteAll() +{ + WriteStatement{Utils::SmallString{"DELETE FROM ", sessionsTableName}, database}.execute(); +} + +} // namespace Sqlite diff --git a/src/libs/sqlite/sqlitesessions.h b/src/libs/sqlite/sqlitesessions.h new file mode 100644 index 00000000000..07d53dbddcf --- /dev/null +++ b/src/libs/sqlite/sqlitesessions.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "sqlitedatabase.h" +#include "sqlitewritestatement.h" + +extern "C" { +typedef struct sqlite3_session sqlite3_session; +void sqlite3session_delete(sqlite3_session *pSession); +}; + +namespace Sqlite { + +namespace Internal { + +class SQLITE_EXPORT SessionsBase +{ +public: + SessionsBase(Database &database, Utils::SmallStringView sessionsTableName) + : sessionsTableName(sessionsTableName) + { + createSessionTable(database); + } + + void createSessionTable(Database &database); + +public: + Utils::SmallString sessionsTableName; +}; +} // namespace Internal + +class SQLITE_EXPORT Sessions : public Internal::SessionsBase +{ +public: + Sessions(Database &database, + Utils::SmallStringView databaseName, + Utils::SmallStringView sessionsTableName) + : SessionsBase(database, sessionsTableName) + , database(database) + , insertSession{Utils::PathString{"INSERT INTO ", + sessionsTableName, + "(changeset) VALUES(?)"}, + database} + , databaseName(databaseName) + , session{nullptr, sqlite3session_delete} + {} + ~Sessions(); + + void setAttachedTables(Utils::SmallStringVector tables); + + void create(); + void commit(); + void rollback(); + + void revert(); + void apply(); + void applyAndUpdateSessions(); + void deleteAll(); + +private: + void attachTables(const Utils::SmallStringVector &tables); + +public: + Database &database; + WriteStatement insertSession; + Utils::SmallString databaseName; + Utils::SmallStringVector tableNames; + std::unique_ptr session; +}; + +} // namespace Sqlite diff --git a/src/libs/sqlite/sqlitetransaction.h b/src/libs/sqlite/sqlitetransaction.h index 5c2c0fe0f8f..d3eee99fb12 100644 --- a/src/libs/sqlite/sqlitetransaction.h +++ b/src/libs/sqlite/sqlitetransaction.h @@ -27,6 +27,8 @@ #include "sqliteglobal.h" +#include + #include #include @@ -49,6 +51,9 @@ public: virtual void rollback() = 0; virtual void lock() = 0; virtual void unlock() = 0; + virtual void immediateSessionBegin() = 0; + virtual void sessionCommit() = 0; + virtual void sessionRollback() = 0; protected: ~TransactionInterface() = default; @@ -82,6 +87,42 @@ protected: bool m_rollback = false; }; +class AbstractThrowingSessionTransaction +{ +public: + AbstractThrowingSessionTransaction(const AbstractTransaction &) = delete; + AbstractThrowingSessionTransaction &operator=(const AbstractTransaction &) = delete; + + void commit() + { + m_interface.sessionCommit(); + m_isAlreadyCommited = true; + m_locker.unlock(); + } + + ~AbstractThrowingSessionTransaction() noexcept(false) + { + try { + if (m_rollback) + m_interface.sessionRollback(); + } catch (...) { + if (!std::uncaught_exception()) + throw; + } + } + +protected: + AbstractThrowingSessionTransaction(TransactionInterface &interface) + : m_interface(interface) + {} + +protected: + TransactionInterface &m_interface; + std::unique_lock m_locker{m_interface}; + bool m_isAlreadyCommited = false; + bool m_rollback = false; +}; + class AbstractThrowingTransaction : public AbstractTransaction { public: @@ -181,6 +222,23 @@ public: }; using ExclusiveTransaction = BasicExclusiveTransaction; -using ExclusiveNonThrowingDestructorTransaction = BasicExclusiveTransaction; +using ExclusiveNonThrowingDestructorTransaction + = BasicExclusiveTransaction; + +class ImmediateSessionTransaction final : public AbstractThrowingSessionTransaction +{ +public: + ImmediateSessionTransaction(TransactionInterface &interface) + : AbstractThrowingSessionTransaction(interface) + { + interface.immediateSessionBegin(); + } + + ~ImmediateSessionTransaction() + { + AbstractThrowingSessionTransaction::m_rollback + = !AbstractThrowingSessionTransaction::m_isAlreadyCommited; + } +}; } // namespace Sqlite diff --git a/src/libs/sqlite/sqlitevalue.h b/src/libs/sqlite/sqlitevalue.h index 953daf3e3a8..58c2902a0ed 100644 --- a/src/libs/sqlite/sqlitevalue.h +++ b/src/libs/sqlite/sqlitevalue.h @@ -30,6 +30,8 @@ #include +#include + #pragma once namespace Sqlite { diff --git a/src/tools/clangpchmanagerbackend/source/builddependenciesstorage.h b/src/tools/clangpchmanagerbackend/source/builddependenciesstorage.h index aac518af5ad..96dd3205bca 100644 --- a/src/tools/clangpchmanagerbackend/source/builddependenciesstorage.h +++ b/src/tools/clangpchmanagerbackend/source/builddependenciesstorage.h @@ -398,7 +398,7 @@ public: "sourceId", database}; mutable ReadStatement fetchIndexingTimeStampsStatement{ - "SELECT sourceId, indexingTimeStamp FROM fileStatuses", database}; + "SELECT sourceId, indexingTimeStamp FROM fileStatuses ORDER BY sourceId", database}; mutable ReadStatement fetchDependentSourceIdsStatement{ "WITH RECURSIVE collectedDependencies(sourceId) AS (VALUES(?) UNION SELECT " "sourceDependencies.sourceId FROM sourceDependencies, collectedDependencies WHERE " diff --git a/tests/unit/unittest/createtablesqlstatementbuilder-test.cpp b/tests/unit/unittest/createtablesqlstatementbuilder-test.cpp index 33231d92c31..49099081a03 100644 --- a/tests/unit/unittest/createtablesqlstatementbuilder-test.cpp +++ b/tests/unit/unittest/createtablesqlstatementbuilder-test.cpp @@ -510,4 +510,24 @@ TEST_F(CreateTableSqlStatementBuilder, GeneratedAlwaysVirtual) "VIRTUAL)"); } +TEST_F(CreateTableSqlStatementBuilder, PrimaryKeyAutoincrement) +{ + builder.clear(); + builder.setTableName("test"); + + builder.addColumn("id", ColumnType::Integer, {Sqlite::PrimaryKey{Sqlite::AutoIncrement::Yes}}); + + ASSERT_THAT(builder.sqlStatement(), "CREATE TABLE test(id INTEGER PRIMARY KEY AUTOINCREMENT)"); +} + +TEST_F(CreateTableSqlStatementBuilder, BlobType) +{ + builder.clear(); + builder.setTableName("test"); + + builder.addColumn("data", ColumnType::Blob); + + ASSERT_THAT(builder.sqlStatement(), "CREATE TABLE test(data BLOB)"); +} + } // namespace diff --git a/tests/unit/unittest/data/sqlite_database.db b/tests/unit/unittest/data/sqlite_database.db index 9c5879579e5..5a7284d0878 100644 Binary files a/tests/unit/unittest/data/sqlite_database.db and b/tests/unit/unittest/data/sqlite_database.db differ diff --git a/tests/unit/unittest/gtest-creator-printing.cpp b/tests/unit/unittest/gtest-creator-printing.cpp index 6d5f7a2cf34..eab506e3447 100644 --- a/tests/unit/unittest/gtest-creator-printing.cpp +++ b/tests/unit/unittest/gtest-creator-printing.cpp @@ -58,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,8 @@ #include #include +#include + namespace { ClangBackEnd::FilePathCaching *filePathCache = nullptr; } @@ -321,6 +324,90 @@ std::ostream &operator<<(std::ostream &out, const Value &value) return out << ")"; } + +namespace { +Utils::SmallStringView operationText(int operation) +{ + switch (operation) { + case SQLITE_INSERT: + return "INSERT"; + case SQLITE_UPDATE: + return "UPDATE"; + case SQLITE_DELETE: + return "DELETE"; + } + + return {}; +} + +std::ostream &operator<<(std::ostream &out, sqlite3_changeset_iter *iter) +{ + out << "("; + + const char *tableName = nullptr; + int columns = 0; + int operation = 0; + sqlite3_value *value = nullptr; + + sqlite3changeset_op(iter, &tableName, &columns, &operation, 0); + + out << operationText(operation) << " " << tableName << " {"; + + if (operation == SQLITE_UPDATE || operation == SQLITE_DELETE) { + out << "Old: ["; + + for (int i = 0; i < columns; i++) { + sqlite3changeset_old(iter, i, &value); + + if (value) + out << " " << sqlite3_value_text(value); + else + out << " -"; + } + out << "]"; + } + + if (operation == SQLITE_UPDATE) + out << ", "; + + if (operation == SQLITE_UPDATE || operation == SQLITE_INSERT) { + out << "New: ["; + for (int i = 0; i < columns; i++) { + sqlite3changeset_new(iter, i, &value); + + if (value) + out << " " << sqlite3_value_text(value); + else + out << " -"; + } + out << "]"; + } + + out << "})"; + + return out; +} +} // namespace + +std::ostream &operator<<(std::ostream &out, const SessionChangeSet &changeset) +{ + sqlite3_changeset_iter *iter = nullptr; + sqlite3changeset_start(&iter, changeset.size, const_cast(changeset.data)); + + out << "ChangeSets(["; + + if (SQLITE_ROW == sqlite3changeset_next(iter)) { + out << iter; + while (SQLITE_ROW == sqlite3changeset_next(iter)) + out << ", " << iter; + } + + sqlite3changeset_finalize(iter); + + out << "])"; + + return out; +} } // namespace Sqlite namespace ClangBackEnd { diff --git a/tests/unit/unittest/gtest-creator-printing.h b/tests/unit/unittest/gtest-creator-printing.h index ea33bc9d497..c791290ae2d 100644 --- a/tests/unit/unittest/gtest-creator-printing.h +++ b/tests/unit/unittest/gtest-creator-printing.h @@ -66,8 +66,10 @@ void PrintTo(const TextRange &range, ::std::ostream *os); namespace Sqlite { class Value; +class SessionChangeSet; std::ostream &operator<<(std::ostream &out, const Value &value); +std::ostream &operator<<(std::ostream &out, const SessionChangeSet &changeset); } // namespace Sqlite namespace ProjectExplorer { diff --git a/tests/unit/unittest/mocksqlitedatabase.h b/tests/unit/unittest/mocksqlitedatabase.h index 97d5394339b..c627f995a4a 100644 --- a/tests/unit/unittest/mocksqlitedatabase.h +++ b/tests/unit/unittest/mocksqlitedatabase.h @@ -63,5 +63,9 @@ public: MOCK_METHOD1(setUpdateHook, void(Sqlite::DatabaseInterface::UpdateCallback &)); MOCK_METHOD0(resetUpdateHook, void()); + + MOCK_METHOD0(applyAndUpdateSessions, void()); + + MOCK_METHOD1(setAttachedTables, void(const Utils::SmallStringVector &tables)); }; diff --git a/tests/unit/unittest/mocksqlitetransactionbackend.h b/tests/unit/unittest/mocksqlitetransactionbackend.h index 0cfeee2892d..363ad573d7a 100644 --- a/tests/unit/unittest/mocksqlitetransactionbackend.h +++ b/tests/unit/unittest/mocksqlitetransactionbackend.h @@ -40,4 +40,7 @@ public: MOCK_METHOD0(rollback, void ()); MOCK_METHOD0(lock, void ()); MOCK_METHOD0(unlock, void ()); + MOCK_METHOD0(immediateSessionBegin, void()); + MOCK_METHOD0(sessionCommit, void()); + MOCK_METHOD0(sessionRollback, void()); }; diff --git a/tests/unit/unittest/sqlitedatabase-test.cpp b/tests/unit/unittest/sqlitedatabase-test.cpp index ff3436c4592..5d7fdb1e540 100644 --- a/tests/unit/unittest/sqlitedatabase-test.cpp +++ b/tests/unit/unittest/sqlitedatabase-test.cpp @@ -28,6 +28,7 @@ #include "spydummy.h" #include +#include #include #include #include @@ -50,26 +51,33 @@ using Sqlite::Table; class SqliteDatabase : public ::testing::Test { protected: - void SetUp() override + SqliteDatabase() { database.setJournalMode(JournalMode::Memory); database.setDatabaseFilePath(databaseFilePath); auto &table = database.addTable(); table.setName("test"); + table.addColumn("id", Sqlite::ColumnType::Integer, {Sqlite::PrimaryKey{}}); table.addColumn("name"); database.open(); } - void TearDown() override + ~SqliteDatabase() { if (database.isOpen()) database.close(); } + std::vector names() const + { + return Sqlite::ReadStatement("SELECT name FROM test", database).values(8); + } + +protected: SpyDummy spyDummy; QString databaseFilePath{":memory:"}; - Sqlite::Database database; + mutable Sqlite::Database database; Sqlite::TransactionInterface &transactionInterface = database; MockFunction callbackMock; Sqlite::Database::UpdateCallback callback = callbackMock.AsStdFunction(); @@ -307,4 +315,33 @@ TEST_F(SqliteDatabase, TableUpdateHookCall) Sqlite::WriteStatement("INSERT INTO test(name) VALUES (?)", database).write(42); } +TEST_F(SqliteDatabase, SessionsCommit) +{ + database.setAttachedTables({"test"}); + Sqlite::WriteStatement("INSERT INTO test(id, name) VALUES (?,?)", database).write(1, "foo"); + + Sqlite::ImmediateSessionTransaction transaction{database}; + Sqlite::WriteStatement("INSERT INTO test(id, name) VALUES (?,?)", database).write(2, "bar"); + transaction.commit(); + Sqlite::WriteStatement("INSERT OR REPLACE INTO test(id, name) VALUES (?,?)", database).write(2, "hoo"); + database.applyAndUpdateSessions(); + + ASSERT_THAT(names(), ElementsAre("foo", "bar")); +} + +TEST_F(SqliteDatabase, SessionsRollback) +{ + database.setAttachedTables({"test"}); + Sqlite::WriteStatement("INSERT INTO test(id, name) VALUES (?,?)", database).write(1, "foo"); + + { + Sqlite::ImmediateSessionTransaction transaction{database}; + Sqlite::WriteStatement("INSERT INTO test(id, name) VALUES (?,?)", database).write(2, "bar"); + } + Sqlite::WriteStatement("INSERT OR REPLACE INTO test(id, name) VALUES (?,?)", database).write(2, "hoo"); + database.applyAndUpdateSessions(); + + ASSERT_THAT(names(), ElementsAre("foo", "hoo")); +} + } // namespace diff --git a/tests/unit/unittest/sqlitesessions-test.cpp b/tests/unit/unittest/sqlitesessions-test.cpp new file mode 100644 index 00000000000..f10dde1bc86 --- /dev/null +++ b/tests/unit/unittest/sqlitesessions-test.cpp @@ -0,0 +1,443 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "googletest.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +using Sqlite::SessionChangeSet; +using Sqlite::SessionChangeSets; + +class DatabaseExecute +{ +public: + DatabaseExecute(Utils::SmallStringView sqlStatement, Sqlite::Database &database) + { + database.execute(sqlStatement); + } +}; + +class Data +{ +public: + Data(Sqlite::ValueView name, Sqlite::ValueView number, Sqlite::ValueView value) + : name(name) + , number(number) + , value(value) + {} + + Sqlite::Value name; + Sqlite::Value number; + Sqlite::Value value; +}; + +std::ostream &operator<<(std::ostream &out, const Data &data) +{ + return out << "(" << data.name << ", " << data.number << " " << data.value << ")"; +} + +MATCHER_P3(HasData, + name, + number, + value, + std::string(negation ? "hasn't " : "has ") + PrintToString(name) + ", " + + PrintToString(number) + ", " + PrintToString(value)) +{ + const Data &data = arg; + + return data.name == name && data.number == number && data.value == value; +} + +class Tag +{ +public: + Tag(Sqlite::ValueView name, Sqlite::ValueView tag) + : name(name) + , tag(tag) + {} + + Sqlite::Value name; + Sqlite::Value tag; +}; + +std::ostream &operator<<(std::ostream &out, const Tag &tag) +{ + return out << "(" << tag.name << ", " << tag.tag << ")"; +} + +MATCHER_P2(HasTag, + name, + tag, + std::string(negation ? "hasn't " : "has ") + PrintToString(name) + ", " + PrintToString(tag)) +{ + const Tag &t = arg; + + return t.name == name && t.tag == tag; +} + +class Sessions : public testing::Test +{ +protected: + Sessions() { sessions.setAttachedTables({"data", "tags"}); } + + std::vector fetchData() { return selectData.values(8); } + std::vector fetchTags() { return selectTags.values(8); } + SessionChangeSets fetchChangeSets() { return selectChangeSets.values(8); } + +protected: + Sqlite::Database database{":memory:", Sqlite::JournalMode::Memory}; + DatabaseExecute createTable{"CREATE TABLE data(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT " + "UNIQUE, number NUMERIC, value NUMERIC)", + database}; + DatabaseExecute createTable2{"CREATE TABLE tags(id INTEGER PRIMARY KEY AUTOINCREMENT, dataId " + "INTEGER NOT NULL REFERENCES data ON DELETE CASCADE DEFERRABLE " + "INITIALLY DEFERRED, tag NUMERIC)", + database}; + Sqlite::Sessions sessions{database, "main", "testsessions"}; + Sqlite::WriteStatement insertData{"INSERT INTO data(name, number, value) VALUES (?1, ?2, ?3) " + "ON CONFLICT (name) DO UPDATE SET (number, value) = (?2, ?3)", + database}; + Sqlite::WriteStatement updateNumber{"UPDATE data SET number = ?002 WHERE name=?001", database}; + Sqlite::WriteStatement updateValue{"UPDATE data SET value = ?002 WHERE name=?001", database}; + Sqlite::WriteStatement deleteData{"DELETE FROM data WHERE name=?", database}; + Sqlite::WriteStatement deleteTag{ + "DELETE FROM tags WHERE dataId=(SELECT id FROM data WHERE name=?)", database}; + Sqlite::WriteStatement insertTag{ + "INSERT INTO tags(dataId, tag) VALUES ((SELECT id FROM data WHERE name=?1), ?2) ", database}; + Sqlite::ReadStatement selectData{"SELECT name, number, value FROM data", database}; + Sqlite::ReadStatement selectTags{"SELECT name, tag FROM tags JOIN data ON data.id=tags.dataId", + database}; + Sqlite::ReadStatement selectChangeSets{"SELECT changeset FROM testsessions", database}; +}; + +TEST_F(Sessions, DontThrowForCommittingWithoutSessionStart) +{ + ASSERT_NO_THROW(sessions.commit()); +} + +TEST_F(Sessions, CreateEmptySession) +{ + sessions.create(); + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), IsEmpty()); +} + +TEST_F(Sessions, CreateSessionWithInsert) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(1)); +} + +TEST_F(Sessions, CreateSessionWithUpdate) +{ + insertData.write("foo", 22, 3.14); + + sessions.create(); + updateNumber.write("foo", "bar"); + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(1)); +} + +TEST_F(Sessions, CreateSessionWithDelete) +{ + insertData.write("foo", 22, 3.14); + + sessions.create(); + deleteData.write("foo"); + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(1)); +} + +TEST_F(Sessions, CreateSessionWithInsertAndUpdate) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + + sessions.create(); + updateNumber.write("foo", "bar"); + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(2)); +} + +TEST_F(Sessions, CreateSession) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + + sessions.commit(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(1)); +} + +TEST_F(Sessions, RevertSession) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + + sessions.revert(); + + ASSERT_THAT(fetchData(), IsEmpty()); +} + +TEST_F(Sessions, RevertSessionToBase) +{ + insertData.write("bar", "foo", 99); + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + + sessions.revert(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("bar", "foo", 99))); +} + +TEST_F(Sessions, RevertMultipleSession) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + sessions.create(); + updateNumber.write("foo", "bar"); + sessions.commit(); + + sessions.revert(); + + ASSERT_THAT(fetchData(), IsEmpty()); +} + +TEST_F(Sessions, ApplySession) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + + sessions.apply(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", 22, 3.14))); +} + +TEST_F(Sessions, ApplySessionAfterAddingNewEntries) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + insertData.write("bar", "foo", 99); + + sessions.apply(); + + ASSERT_THAT(fetchData(), + UnorderedElementsAre(HasData("foo", 22, 3.14), HasData("bar", "foo", 99))); +} + +TEST_F(Sessions, ApplyOverridesEntriesWithUniqueConstraint) +{ + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + insertData.write("foo", "bar", 3.14); + + sessions.apply(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", 22, 3.14))); +} + +TEST_F(Sessions, ApplyDoesNotOverrideDeletedEntries) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + insertData.write("foo", 22, 3.14); + sessions.commit(); + deleteData.write("foo"); + + sessions.apply(); + + ASSERT_THAT(fetchData(), IsEmpty()); +} + +TEST_F(Sessions, ApplyDoesOnlyOverwriteUpdatedValues) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 1234); + sessions.commit(); + insertData.write("foo", "poo", 891); + + sessions.apply(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", "poo", 1234))); +} + +TEST_F(Sessions, ApplyDoesDoesNotOverrideForeignKeyIfReferenceIsDeleted) +{ + insertData.write("foo2", "bar", 3.14); + insertData.write("foo", "bar", 3.14); + sessions.create(); + insertTag.write("foo2", 4321); + insertTag.write("foo", 1234); + sessions.commit(); + deleteData.write("foo"); + + sessions.apply(); + + ASSERT_THAT(fetchTags(), ElementsAre(HasTag("foo2", 4321))); +} + +TEST_F(Sessions, ApplyDoesDoesNotOverrideIfConstraintsIsApplied) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + deleteData.write("foo"); + sessions.commit(); + sessions.revert(); + insertTag.write("foo", 1234); + + sessions.apply(); + + ASSERT_THAT(fetchTags(), IsEmpty()); +} + +TEST_F(Sessions, ApplyDoesDoesNotOverrideForeignKeyIfReferenceIsDeletedDeferred) +{ + Sqlite::DeferredTransaction transaction{database}; + insertData.write("foo2", "bar", 3.14); + insertData.write("foo", "bar", 3.14); + sessions.create(); + insertTag.write("foo2", 4321); + insertTag.write("foo", 1234); + sessions.commit(); + deleteData.write("foo"); + + sessions.apply(); + + transaction.commit(); + ASSERT_THAT(fetchTags(), ElementsAre(HasTag("foo2", 4321))); +} + +TEST_F(Sessions, EndSessionOnRollback) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.rollback(); + sessions.commit(); + sessions.create(); + updateNumber.write("foo", 333); + sessions.commit(); + updateValue.write("foo", 666); + + sessions.apply(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", 333, 666))); +} + +TEST_F(Sessions, EndSessionOnCommit) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.commit(); + updateValue.write("foo", 666); + sessions.commit(); + + sessions.apply(); + + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", "bar", 99))); +} + +TEST_F(Sessions, DeleteSessions) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.commit(); + sessions.revert(); + + sessions.deleteAll(); + + sessions.apply(); + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", "bar", 3.14))); +} + +TEST_F(Sessions, DeleteAllSessions) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.commit(); + sessions.revert(); + + sessions.deleteAll(); + + sessions.apply(); + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", "bar", 3.14))); +} + +TEST_F(Sessions, ApplyAndUpdateSessions) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.commit(); + updateValue.write("foo", 99); + + sessions.applyAndUpdateSessions(); + + updateValue.write("foo", 22); + sessions.apply(); + ASSERT_THAT(fetchData(), ElementsAre(HasData("foo", "bar", 22))); +} + +TEST_F(Sessions, ApplyAndUpdateSessionsHasOnlyOneChangeSet) +{ + insertData.write("foo", "bar", 3.14); + sessions.create(); + updateValue.write("foo", 99); + sessions.commit(); + updateValue.write("foo", 99); + + sessions.applyAndUpdateSessions(); + + ASSERT_THAT(fetchChangeSets(), SizeIs(1)); +} + +} // namespace diff --git a/tests/unit/unittest/sqlitestatement-test.cpp b/tests/unit/unittest/sqlitestatement-test.cpp index 4b59d52af29..a7dd5d5f9ba 100644 --- a/tests/unit/unittest/sqlitestatement-test.cpp +++ b/tests/unit/unittest/sqlitestatement-test.cpp @@ -460,9 +460,8 @@ TEST_F(SqliteStatement, GetTupleValuesWithoutArguments) auto values = statement.values(3); - ASSERT_THAT(values, ElementsAre(Tuple{"bar", 0, 1}, - Tuple{"foo", 23.3, 2}, - Tuple{"poo", 40.0, 3})); + ASSERT_THAT(values, + UnorderedElementsAre(Tuple{"bar", 0, 1}, Tuple{"foo", 23.3, 2}, Tuple{"poo", 40.0, 3})); } TEST_F(SqliteStatement, GetSingleValuesWithoutArguments) @@ -471,7 +470,7 @@ TEST_F(SqliteStatement, GetSingleValuesWithoutArguments) std::vector values = statement.values(3); - ASSERT_THAT(values, ElementsAre("bar", "foo", "poo")); + ASSERT_THAT(values, UnorderedElementsAre("bar", "foo", "poo")); } class FooValue @@ -497,7 +496,7 @@ TEST_F(SqliteStatement, GetSingleSqliteValuesWithoutArguments) std::vector values = statement.values(3); - ASSERT_THAT(values, ElementsAre(Eq("blah"), Eq(23.3), Eq(40), IsNull())); + ASSERT_THAT(values, UnorderedElementsAre(Eq("blah"), Eq(23.3), Eq(40), IsNull())); } TEST_F(SqliteStatement, GetStructValuesWithoutArguments) @@ -506,9 +505,10 @@ TEST_F(SqliteStatement, GetStructValuesWithoutArguments) auto values = statement.values(3); - ASSERT_THAT(values, ElementsAre(Output{"bar", "blah", 1}, - Output{"foo", "23.3", 2}, - Output{"poo", "40", 3})); + ASSERT_THAT(values, + UnorderedElementsAre(Output{"bar", "blah", 1}, + Output{"foo", "23.3", 2}, + Output{"poo", "40", 3})); } TEST_F(SqliteStatement, GetValuesForSingleOutputWithBindingMultipleTimes) @@ -529,8 +529,7 @@ TEST_F(SqliteStatement, GetValuesForMultipleOutputValuesAndContainerQueryValues) auto values = statement.values(3, queryValues); - ASSERT_THAT(values, ElementsAre(Tuple{"poo", 40, 3.}, - Tuple{"foo", 23.3, 2.})); + ASSERT_THAT(values, UnorderedElementsAre(Tuple{"poo", 40, 3.}, Tuple{"foo", 23.3, 2.})); } TEST_F(SqliteStatement, GetValuesForSingleOutputValuesAndContainerQueryValues) @@ -540,7 +539,7 @@ TEST_F(SqliteStatement, GetValuesForSingleOutputValuesAndContainerQueryValues) std::vector values = statement.values(3, queryValues); - ASSERT_THAT(values, ElementsAre("poo", "foo")); + ASSERT_THAT(values, UnorderedElementsAre("poo", "foo")); } TEST_F(SqliteStatement, GetValuesForMultipleOutputValuesAndContainerQueryTupleValues) @@ -552,8 +551,7 @@ TEST_F(SqliteStatement, GetValuesForMultipleOutputValuesAndContainerQueryTupleVa auto values = statement.values(3, queryValues); - ASSERT_THAT(values, ElementsAre(ResultTuple{"poo", 40, 3}, - ResultTuple{"bar", 0, 1})); + ASSERT_THAT(values, UnorderedElementsAre(ResultTuple{"poo", 40, 3}, ResultTuple{"bar", 0, 1})); } TEST_F(SqliteStatement, GetValuesForSingleOutputValuesAndContainerQueryTupleValues) @@ -564,7 +562,7 @@ TEST_F(SqliteStatement, GetValuesForSingleOutputValuesAndContainerQueryTupleValu std::vector values = statement.values(3, queryValues); - ASSERT_THAT(values, ElementsAre("poo", "bar")); + ASSERT_THAT(values, UnorderedElementsAre("poo", "bar")); } TEST_F(SqliteStatement, GetValuesForMultipleOutputValuesAndMultipleQueryValue) @@ -616,8 +614,7 @@ TEST_F(SqliteStatement, GetStructOutputValuesAndContainerQueryTupleValues) auto values = statement.values(3, queryValues); - ASSERT_THAT(values, ElementsAre(Output{"poo", "40", 3}, - Output{"bar", "blah", 1})); + ASSERT_THAT(values, UnorderedElementsAre(Output{"poo", "40", 3}, Output{"bar", "blah", 1})); } TEST_F(SqliteStatement, GetBlobValues) diff --git a/tests/unit/unittest/sqlitetransaction-test.cpp b/tests/unit/unittest/sqlitetransaction-test.cpp index dc3e5bba70b..0aa426db675 100644 --- a/tests/unit/unittest/sqlitetransaction-test.cpp +++ b/tests/unit/unittest/sqlitetransaction-test.cpp @@ -33,12 +33,13 @@ namespace { -using Sqlite::DeferredTransaction; -using Sqlite::ImmediateTransaction; -using Sqlite::ExclusiveTransaction; using Sqlite::DeferredNonThrowingDestructorTransaction; -using Sqlite::ImmediateNonThrowingDestructorTransaction; +using Sqlite::DeferredTransaction; using Sqlite::ExclusiveNonThrowingDestructorTransaction; +using Sqlite::ExclusiveTransaction; +using Sqlite::ImmediateNonThrowingDestructorTransaction; +using Sqlite::ImmediateSessionTransaction; +using Sqlite::ImmediateTransaction; class SqliteTransaction : public testing::Test { @@ -316,4 +317,56 @@ TEST_F(SqliteTransaction, TransactionRollbackInDestructorDontThrows) ASSERT_NO_THROW(ExclusiveNonThrowingDestructorTransaction{mockTransactionBackend}); } +TEST_F(SqliteTransaction, ImmediateSessionTransactionCommit) +{ + InSequence s; + + EXPECT_CALL(mockTransactionBackend, lock()); + EXPECT_CALL(mockTransactionBackend, immediateSessionBegin()); + EXPECT_CALL(mockTransactionBackend, sessionCommit()); + EXPECT_CALL(mockTransactionBackend, unlock()); + + ImmediateSessionTransaction transaction{mockTransactionBackend}; + transaction.commit(); } + +TEST_F(SqliteTransaction, ImmediateSessionTransactionRollBack) +{ + InSequence s; + + EXPECT_CALL(mockTransactionBackend, lock()); + EXPECT_CALL(mockTransactionBackend, immediateSessionBegin()); + EXPECT_CALL(mockTransactionBackend, sessionRollback()); + EXPECT_CALL(mockTransactionBackend, unlock()); + + ImmediateSessionTransaction transaction{mockTransactionBackend}; +} + +TEST_F(SqliteTransaction, SessionTransactionRollbackInDestructorThrows) +{ + ON_CALL(mockTransactionBackend, sessionRollback()).WillByDefault(Throw(Sqlite::Exception("foo"))); + + ASSERT_THROW(ImmediateSessionTransaction{mockTransactionBackend}, Sqlite::Exception); +} + +TEST_F(SqliteTransaction, ImmidiateSessionTransactionBeginThrows) +{ + ON_CALL(mockTransactionBackend, immediateSessionBegin()) + .WillByDefault(Throw(Sqlite::Exception("foo"))); + + ASSERT_THROW(ImmediateSessionTransaction{mockTransactionBackend}, Sqlite::Exception); +} + +TEST_F(SqliteTransaction, ImmediateSessionTransactionBeginThrowsAndNotRollback) +{ + InSequence s; + + EXPECT_CALL(mockTransactionBackend, lock()); + EXPECT_CALL(mockTransactionBackend, immediateSessionBegin()).WillOnce(Throw(Sqlite::Exception("foo"))); + EXPECT_CALL(mockTransactionBackend, sessionRollback()).Times(0); + EXPECT_CALL(mockTransactionBackend, unlock()); + + ASSERT_ANY_THROW(ImmediateSessionTransaction{mockTransactionBackend}); +} + +} // namespace diff --git a/tests/unit/unittest/unittest.pro b/tests/unit/unittest/unittest.pro index 7340f007754..03f04f39824 100644 --- a/tests/unit/unittest/unittest.pro +++ b/tests/unit/unittest/unittest.pro @@ -81,6 +81,7 @@ SOURCES += \ smallstring-test.cpp \ sourcerangefilter-test.cpp \ spydummy.cpp \ + sqlitesessions-test.cpp \ sqlitevalue-test.cpp \ symbolindexer-test.cpp \ symbolsfindfilter-test.cpp \ diff --git a/tests/unit/unittest/unittests-main.cpp b/tests/unit/unittest/unittests-main.cpp index f5eaa8846c2..6704a08034c 100644 --- a/tests/unit/unittest/unittests-main.cpp +++ b/tests/unit/unittest/unittests-main.cpp @@ -25,8 +25,8 @@ #include "googletest.h" +#include #include - #include #include @@ -38,6 +38,7 @@ int main(int argc, char *argv[]) { + Sqlite::Database::activateLogging(); const QString temporayDirectoryPath = QDir::tempPath() +"/QtCreator-UnitTests-XXXXXX"; Utils::TemporaryDirectory::setMasterTemporaryDirectory(temporayDirectoryPath); qputenv("TMPDIR", Utils::TemporaryDirectory::masterDirectoryPath().toUtf8());