diff --git a/CMakeLists.txt b/CMakeLists.txt index fb92a16..4b27bd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT cmake_minimum_required(VERSION 3.16) -project(libasql VERSION 0.91.0 LANGUAGES CXX) +project(libasql VERSION 0.92.0 LANGUAGES CXX) include(GNUInstallDirs) @@ -22,7 +22,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) # to always look for includes there: set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/demos/async1/CMakeLists.txt b/demos/async1/CMakeLists.txt index 88f1e21..1cbde21 100644 --- a/demos/async1/CMakeLists.txt +++ b/demos/async1/CMakeLists.txt @@ -8,6 +8,13 @@ target_link_libraries(async1 Qt::Core ) +add_executable(coroutines coroutines.cpp) +target_link_libraries(coroutines + ASql::Core + ASql::Pg + Qt::Core +) + add_executable(transactions transactions.cpp) target_link_libraries(transactions ASql::Core diff --git a/demos/async1/coroutines.cpp b/demos/async1/coroutines.cpp new file mode 100644 index 0000000..5f02020 --- /dev/null +++ b/demos/async1/coroutines.cpp @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: (C) 2020 Daniel Nicoletti + * SPDX-License-Identifier: MIT + */ + +#include "../../src/acache.h" +#include "../../src/acoroexpected.h" +#include "../../src/adatabase.h" +#include "../../src/amigrations.h" +#include "../../src/apg.h" +#include "../../src/apool.h" +#include "../../src/aresult.h" +#include "../../src/atransaction.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ASql; +using namespace Qt::StringLiterals; + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + APool::create(APg::factory(u"postgres:///?target_session_attrs=read-write"_s)); + APool::setMaxIdleConnections(10); + + auto db = APool::database(); + + auto counter = std::make_shared(0); + QElapsedTimer t; + if (false) { + t.start(); + for (int i = 0; i < 100'000; ++i) { + db.exec(u"SELECT 1", nullptr, [counter, &t](AResult &result) { + if (*counter == 999) { + qDebug() << "lambda" << t.elapsed(); + } + *counter = *counter + 1; + }); + } + + *counter = 0; + t.start(); + auto bench = [&t, counter, &db]() -> ACoroTerminator { + auto counter = std::make_shared(); + for (int i = 0; i < 100'000; ++i) { + auto result = co_await db.coExec(u"SELECT 1", nullptr); + // qDebug() << "coroutine" << *counter << t.elapsed(); + if (*counter == 999) { + qDebug() << "coroutine" << *counter << t.elapsed(); + } + *counter = *counter + 1; + } + }; + bench(); + } + + if (false) { + auto callEx = []() -> ACoroTerminator { + qDebug() << "coro started"; + + auto db = APool::database(); + db.exec(u"SELECT 2", nullptr, [](AResult &result) { + if (result.error()) { + qDebug() << "Error" << result.errorString(); + } else { + qDebug() << "1s loop" << result.toHash(); + } + }); + + auto obj = new QObject; + QTimer::singleShot(500, obj, [obj] { + qDebug() << "Delete Obj"; + delete obj; + }); + + { + auto result = co_await db.coExec(u8"SELECT now(), pg_sleep(3)", obj); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + obj->setProperty("crash", true); + } + + { + auto result = co_await db.coExec(u8"SELECT now(), pg_sleep(1)", nullptr); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + obj->setProperty("crash", true); + } + }; + + callEx(); + } + + if (false) { + auto callTransaction = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro exited"; }); + qDebug() << "coro started"; + + auto db = co_await APool::coDatabase(); + + auto transaction = co_await db->coBegin(); + qDebug() << "transaction started"; + + auto result = co_await db->coExec(u8"SELECT now()"); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + + auto result2 = co_await db->coExec(u8"SELECT now()"); + if (result2.has_value()) { + qDebug() << "coro result2 has value" << result2->toJsonObject(); + } else { + qDebug() << "coro result2 error" << result2.error(); + } + + auto commit = co_await transaction->coCommit(); + if (commit.has_value()) { + qDebug() << "coro commit has value" << commit->toJsonObject(); + } else { + qDebug() << "coro commit error" << commit.error(); + } + }; + + callTransaction(); + } + + if (false) { + auto cache = new ACache; + cache->setDatabase(db); + + auto callCache = [cache]() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro cache exited"; }); + qDebug() << "coro cache started"; + + auto result = co_await cache->coExec(u"SELECT now(), pg_sleep(1)"_s, nullptr); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + + auto resultCached = co_await cache->coExec(u"SELECT now(), pg_sleep(1)"_s, nullptr); + if (resultCached.has_value()) { + qDebug() << "coro resultCached has value" << resultCached->toJsonObject(); + } else { + qDebug() << "coro resultCached error" << resultCached.error(); + } + }; + + callCache(); + } + + if (false) { + auto callInvalid = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro db invalid exited"; }); + qDebug() << "coro db invalid started"; + + ADatabase db; + auto result = co_await db.coExec(u"SELECT now()"_s, nullptr); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + }; + + callInvalid(); + } + + if (true) { + auto callOuter = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro outer exited"; }); + qDebug() << "coro outer started"; + + auto callInner = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro inner exited"; }); + qDebug() << "coro inner started"; + + auto db = co_await APool::coDatabase(); + if (!db) { + qDebug() << "coro db error" << db.error(); + co_return; + } + + auto result = co_await db->coExec(u"SELECT now()"_s, nullptr); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + }; + + // Cannot be awaited :) + callInner(); + co_return; + }; + + callOuter(); + } + + if (false) { + auto callPool = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro pool exited"; }); + qDebug() << "coro pool started"; + + auto db = co_await APool::coDatabase(); + if (db.has_value()) { + qDebug() << "coro pool has value" << db->isOpen(); + } else { + qDebug() << "coro pool error" << db.error(); + } + + auto result = co_await db->coExec(u"SELECT now(), pg_sleep(1)"_s, nullptr); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + }; + + callPool(); + } + + if (false) { + auto callTerminatorEarly = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro exited"; }); + qDebug() << "coro started"; + + auto obj = new QObject; + QTimer::singleShot(500, obj, [obj] { + qDebug() << "Delete Obj"; + delete obj; + }); + co_yield obj; // so that this promise is destroyed if this object is destroyed + + auto db = co_await APool::coDatabase(); + + auto result = co_await db->coExec(u8"SELECT now(), pg_sleep(2)", obj); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + }; + callTerminatorEarly(); + + auto callTerminatorLater = []() -> ACoroTerminator { + auto _ = qScopeGuard([] { qDebug() << "coro exited"; }); + qDebug() << "coro started"; + + auto obj = new QObject; + QTimer::singleShot(2000, obj, [obj] { + qDebug() << "Delete Obj later"; + delete obj; + }); + co_yield obj; // so that this promise is destroyed if this object is destroyed + + auto db = co_await APool::coDatabase(); + + auto result = co_await db->coExec(u8"SELECT now()", obj); + if (result.has_value()) { + qDebug() << "coro result has value" << result->toJsonObject(); + } else { + qDebug() << "coro result error" << result.error(); + } + }; + + callTerminatorLater(); + } + + app.exec(); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a04e293..122bcb9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ set(asql_SRC acache.cpp apreparedquery.cpp apreparedquery.h + acoroexpected.cpp ) set(asql_HEADERS @@ -21,7 +22,7 @@ set(asql_HEADERS apreparedquery.h apool.h atransaction.h - + acoroexpected.h aresult.h adriver.h adriverfactory.h diff --git a/src/acache.cpp b/src/acache.cpp index 1c4a878..e2a6515 100644 --- a/src/acache.cpp +++ b/src/acache.cpp @@ -5,6 +5,7 @@ #include "acache.h" +#include "acoroexpected.h" #include "adatabase.h" #include "apool.h" #include "aresult.h" @@ -249,6 +250,38 @@ int ACache::size() const return d->cache.size(); } +AExpectedResult ACache::coExec(QStringView query, QObject *receiver) +{ + AExpectedResult coro(receiver); + execExpiring(query, -1ms, {}, receiver, coro.callback); + return coro; +} + +AExpectedResult ACache::coExec(QStringView query, const QVariantList &args, QObject *receiver) +{ + AExpectedResult coro(receiver); + execExpiring(query, -1ms, args, receiver, coro.callback); + return coro; +} + +AExpectedResult + ACache::coExecExpiring(QStringView query, std::chrono::milliseconds maxAge, QObject *receiver) +{ + AExpectedResult coro(receiver); + execExpiring(query, maxAge, {}, receiver, coro.callback); + return coro; +} + +AExpectedResult ACache::coExecExpiring(QStringView query, + std::chrono::milliseconds maxAge, + const QVariantList &args, + QObject *receiver) +{ + AExpectedResult coro(receiver); + execExpiring(query, maxAge, args, receiver, coro.callback); + return coro; +} + void ACache::exec(QStringView query, QObject *receiver, AResultFn cb) { execExpiring(query, -1ms, {}, receiver, cb); diff --git a/src/acache.h b/src/acache.h index 2fe0e00..1046602 100644 --- a/src/acache.h +++ b/src/acache.h @@ -12,6 +12,11 @@ namespace ASql { +template +class ACoroExpected; + +using AExpectedResult = ACoroExpected; + class ACachePrivate; class ASQL_EXPORT ACache : public QObject { @@ -43,6 +48,17 @@ class ASQL_EXPORT ACache : public QObject */ [[nodiscard]] int size() const; + AExpectedResult coExec(QStringView query, QObject *receiver = nullptr); + AExpectedResult + coExec(QStringView query, const QVariantList &args, QObject *receiver = nullptr); + AExpectedResult coExecExpiring(QStringView query, + std::chrono::milliseconds maxAge, + QObject *receiver = nullptr); + AExpectedResult coExecExpiring(QStringView query, + std::chrono::milliseconds maxAge, + const QVariantList &args, + QObject *receiver = nullptr); + void exec(QStringView query, QObject *receiver, AResultFn cb); void exec(QStringView query, const QVariantList &args, QObject *receiver, AResultFn cb); void execExpiring(QStringView query, diff --git a/src/acoroexpected.cpp b/src/acoroexpected.cpp new file mode 100644 index 0000000..2979cd7 --- /dev/null +++ b/src/acoroexpected.cpp @@ -0,0 +1,4 @@ +#include "acoroexpected.h" + +using namespace ASql; +using namespace Qt::StringLiterals; diff --git a/src/acoroexpected.h b/src/acoroexpected.h new file mode 100644 index 0000000..e8a4d47 --- /dev/null +++ b/src/acoroexpected.h @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace ASql { + +template +class ASQL_EXPORT ACoroExpected +{ +public: + bool await_ready() const noexcept + { + return m_result.has_value() || !m_result.error().isEmpty(); // ACache skips suspension + } + + bool await_suspend(std::coroutine_handle<> h) noexcept + { + m_handle = h; + if (m_receiver) { + m_destroyConn = QObject::connect(m_receiver, &QObject::destroyed, [h, this] { + m_result = std::unexpected(QStringLiteral("QObject receiver* destroyed")); + h.resume(); + }); + } + + return !await_ready(); + } + + std::expected await_resume() { return m_result; } + + ACoroExpected(QObject *receiver) + : m_receiver(receiver) + , m_result{std::unexpected(QString{})} + { + callback = [this](AResult &result) { + if (result.error()) { + m_result = std::unexpected(result.errorString()); + } else { + if constexpr (std::is_same_v) { + m_result = ATransaction(database, true); + } else { + m_result = result; + } + } + + if (m_handle) { + m_handle.resume(); + } + }; + } + + ~ACoroExpected() { QObject::disconnect(m_destroyConn); } + +protected: + friend class ADatabase; + friend class ACache; + friend class ATransaction; + std::function callback; + ADatabase database; + +private: + QMetaObject::Connection m_destroyConn; + QPointer m_receiver; + std::expected m_result; + std::coroutine_handle<> m_handle; +}; + +class ASQL_EXPORT AExpectedDatabase +{ +public: + bool await_ready() const noexcept + { + return m_result.has_value() || !m_result.error().isEmpty(); + } + + bool await_suspend(std::coroutine_handle<> h) noexcept + { + m_handle = h; + if (m_receiver) { + m_destroyConn = QObject::connect(m_receiver, &QObject::destroyed, [this] { + m_result = std::unexpected(QStringLiteral("QObject receiver* destroyed")); + if (m_handle) { + m_handle.resume(); + } + }); + } + + return !await_ready(); + } + + std::expected await_resume() { return m_result; } + + AExpectedDatabase(QObject *receiver) + : m_receiver(receiver) + , m_result{std::unexpected(QString{})} + { + callback = [this](ADatabase db) { + if (db.isValid()) { + m_result = db; + } else { + m_result = + std::unexpected(QStringLiteral("Could not get a valid database connection")); + } + + if (m_handle) { + m_handle.resume(); + } + }; + } + + ~AExpectedDatabase() { QObject::disconnect(m_destroyConn); } + +protected: + friend class APool; + std::function callback; + +private: + QMetaObject::Connection m_destroyConn; + QPointer m_receiver; + std::expected m_result; + std::coroutine_handle<> m_handle; +}; + +/** + * @brief The ACoroTerminator class + * co_yield object; that should destroy this corouting + * on their desctruction, note that if the caller of the + * coroutine delete object; this might result into a double + * free due coroutine dtor already be on the stack, in such + * cases always object->deleteLater(); + */ +class ACoroTerminator +{ +public: + struct promise_type { + std::coroutine_handle handle; + std::vector connections; + + void clean() + { + for (auto &conn : connections) { + QObject::disconnect(conn); + } + connections.clear(); + } + + void return_void() noexcept {} + + ACoroTerminator get_return_object() + { + handle = std::coroutine_handle::from_promise(*this); + return {}; + } + + std::suspend_never initial_suspend() const noexcept { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void unhandled_exception() {} + + bool await_ready() const noexcept { return false; } + + std::suspend_never yield_value(QObject *obj) + { + auto conn = QObject::connect(obj, &QObject::destroyed, [this] { + clean(); + if (handle) { + handle.destroy(); + } + }); + connections.emplace_back(std::move(conn)); + return {}; + } + void await_suspend(std::coroutine_handle<> h) noexcept {} + void await_resume() const noexcept {} + + ~promise_type() { clean(); } + }; +}; + +} // namespace ASql diff --git a/src/adatabase.cpp b/src/adatabase.cpp index c7a23ec..a09666b 100644 --- a/src/adatabase.cpp +++ b/src/adatabase.cpp @@ -5,8 +5,10 @@ #include "adatabase.h" +#include "acoroexpected.h" #include "adriver.h" #include "adriverfactory.h" +#include "atransaction.h" #include @@ -84,6 +86,15 @@ void ADatabase::begin(QObject *receiver, AResultFn cb) d->begin(d, receiver, cb); } +AExpectedTransaction ADatabase::coBegin(QObject *receiver) +{ + Q_ASSERT(d); + AExpectedTransaction coro(receiver); + coro.database = d; + d->begin(d, receiver, coro.callback); + return coro; +} + void ADatabase::commit(QObject *receiver, AResultFn cb) { Q_ASSERT(d); @@ -96,6 +107,56 @@ void ADatabase::rollback(QObject *receiver, AResultFn cb) d->rollback(d, receiver, cb); } +AExpectedResult ADatabase::coExec(QStringView query, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, QVariantList(), receiver, coro.callback); + return coro; +} + +AExpectedResult ADatabase::coExec(QStringView query, const QVariantList ¶ms, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, params, receiver, coro.callback); + return coro; +} + +AExpectedResult ADatabase::coExec(QUtf8StringView query, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, QVariantList(), receiver, coro.callback); + return coro; +} + +AExpectedResult + ADatabase::coExec(QUtf8StringView query, const QVariantList ¶ms, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, params, receiver, coro.callback); + return coro; +} + +AExpectedResult ADatabase::coExec(const APreparedQuery &query, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, QVariantList(), receiver, coro.callback); + return coro; +} + +AExpectedResult + ADatabase::coExec(const APreparedQuery &query, const QVariantList ¶ms, QObject *receiver) +{ + Q_ASSERT(d); + AExpectedResult coro(receiver); + d->exec(d, query, params, receiver, coro.callback); + return coro; +} + void ADatabase::exec(QStringView query, QObject *receiver, AResultFn cb) { Q_ASSERT(d); diff --git a/src/adatabase.h b/src/adatabase.h index 50c0ef2..716d08d 100644 --- a/src/adatabase.h +++ b/src/adatabase.h @@ -15,6 +15,7 @@ namespace ASql { class AResult; +class ATransaction; class ADriver; class ADriverFactory; @@ -29,6 +30,12 @@ class ADatabaseNotification using AResultFn = std::function; using ANotificationFn = std::function; +template +class ACoroExpected; + +using AExpectedResult = ACoroExpected; +using AExpectedTransaction = ACoroExpected; + class APreparedQuery; class ASQL_EXPORT ADatabase { @@ -123,6 +130,8 @@ class ASQL_EXPORT ADatabase */ void begin(QObject *receiver = nullptr, AResultFn cb = {}); + AExpectedTransaction coBegin(QObject *receiver = nullptr); + /*! * \brief commit a transaction, this operation usually succeeds, * but one can hook up a callback to check it's result. @@ -139,6 +148,22 @@ class ASQL_EXPORT ADatabase */ void rollback(QObject *receiver = nullptr, AResultFn cb = {}); + AExpectedResult coExec(QStringView query, QObject *receiver = nullptr); + + AExpectedResult + coExec(QStringView query, const QVariantList ¶ms, QObject *receiver = nullptr); + + AExpectedResult coExec(QUtf8StringView query, QObject *receiver = nullptr); + + AExpectedResult + coExec(QUtf8StringView query, const QVariantList ¶ms, QObject *receiver = nullptr); + + AExpectedResult coExec(const APreparedQuery &query, QObject *receiver = nullptr); + + AExpectedResult coExec(const APreparedQuery &query, + const QVariantList ¶ms, + QObject *receiver = nullptr); + /*! * \brief exec excutes a \param query against this database connection, * once done AResult object will have the retrieved data if any, always diff --git a/src/apool.cpp b/src/apool.cpp index 269b9be..34f4d10 100644 --- a/src/apool.cpp +++ b/src/apool.cpp @@ -5,6 +5,7 @@ #include "apool.h" +#include "acoroexpected.h" #include "adriver.h" #include "adriverfactory.h" @@ -207,6 +208,13 @@ void APool::database(QObject *receiver, ADatabaseFn cb, QStringView poolName) } } +AExpectedDatabase APool::coDatabase(QObject *receiver, QStringView poolName) +{ + AExpectedDatabase coro(receiver); + database(receiver, coro.callback, poolName); + return coro; +} + void APool::setMaxIdleConnections(int max, QStringView poolName) { auto it = m_connectionPool.find(poolName); diff --git a/src/apool.h b/src/apool.h index 0b7a948..695bbd6 100644 --- a/src/apool.h +++ b/src/apool.h @@ -15,6 +15,8 @@ namespace ASql { using ADatabaseFn = std::function; +class AExpectedDatabase; + class ASQL_EXPORT APool { public: @@ -88,6 +90,9 @@ class ASQL_EXPORT APool */ static void database(QObject *receiver, ADatabaseFn cb, QStringView poolName = defaultPool); + static AExpectedDatabase coDatabase(QObject *receiver = nullptr, + QStringView poolName = defaultPool); + /*! * \brief setMaxIdleConnections maximum number of idle connections of the pool * diff --git a/src/atransaction.cpp b/src/atransaction.cpp index 5a1755f..2a0fb04 100644 --- a/src/atransaction.cpp +++ b/src/atransaction.cpp @@ -5,6 +5,8 @@ #include "atransaction.h" +#include "acoroexpected.h" + #include Q_LOGGING_CATEGORY(ASQL_TRANSACTION, "asql.transaction", QtInfoMsg) @@ -38,6 +40,12 @@ ATransaction::ATransaction() = default; ATransaction::~ATransaction() = default; +ATransaction::ATransaction(const ADatabase &db, bool started) + : ATransaction{db} +{ + d->running = started; +} + ATransaction::ATransaction(const ADatabase &db) : d(std::make_shared(db)) { @@ -88,6 +96,14 @@ void ATransaction::commit(QObject *receiver, AResultFn cb) } } +AExpectedResult ATransaction::coCommit(QObject *receiver) +{ + d->running = false; + AExpectedResult coro(receiver); + d->db.commit(receiver, coro.callback); + return coro; +} + void ATransaction::rollback(QObject *receiver, AResultFn cb) { Q_ASSERT(d); @@ -98,3 +114,12 @@ void ATransaction::rollback(QObject *receiver, AResultFn cb) qWarning(ASQL_TRANSACTION, "Transaction not started"); } } + +AExpectedResult ATransaction::coRollback(QObject *receiver) +{ + Q_ASSERT(d); + d->running = false; + AExpectedResult coro(receiver); + d->db.rollback(receiver, coro.callback); + return coro; +} diff --git a/src/atransaction.h b/src/atransaction.h index a3edb64..65bccb7 100644 --- a/src/atransaction.h +++ b/src/atransaction.h @@ -9,6 +9,11 @@ namespace ASql { +template +class ACoroExpected; + +using AExpectedResult = ACoroExpected; + class ATransactionPrivate; class ASQL_EXPORT ATransaction { @@ -54,6 +59,8 @@ class ASQL_EXPORT ATransaction */ void commit(QObject *receiver = nullptr, AResultFn cb = {}); + AExpectedResult coCommit(QObject *receiver = nullptr); + /*! * \brief rollback a transaction, this operation usually succeeds, * but one can hook up a callback to check it's result. @@ -66,6 +73,12 @@ class ASQL_EXPORT ATransaction */ void rollback(QObject *receiver = nullptr, AResultFn cb = {}); + AExpectedResult coRollback(QObject *receiver = nullptr); + +protected: + friend class ACoroExpected; + ATransaction(const ADatabase &db, bool started); + private: std::shared_ptr d; };