Skip to content

Commit

Permalink
Merge pull request #3 from yowidin/feature/transactions
Browse files Browse the repository at this point in the history
Transactions support
  • Loading branch information
yowidin committed May 19, 2023
2 parents 4f379cf + f4ea607 commit 71e767d
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 16 deletions.
46 changes: 46 additions & 0 deletions .ci/pr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3

import os
import sys
from subprocess import run
from argparse import ArgumentParser


def do_run(*args):
print(f"--- running: {' '.join(*args)} ---")
sys.stdout.flush()
run(*args, check=True)


def main():
parser = ArgumentParser('Project builder')
parser.add_argument('preset', help='Build preset')
parser.add_argument('shared', help='Controls whether a shared library should be built')

args = parser.parse_args()

preset = args.preset
preset_name = f'conan-{preset.lower()}'
shared = args.shared

source_dir = os.getcwd()
build_dir = os.path.join(source_dir, 'build', args.preset)
toolchain_path = os.path.join(build_dir, 'generators', 'conan_toolchain.cmake')

if not os.path.exists(build_dir):
os.makedirs(build_dir)

do_run(['conan', 'install', '-b', 'missing', '-s', 'compiler.cppstd=17', '-s',
f'build_type={preset}', '-c:h', 'tools.cmake.cmake_layout:build_folder_vars=["settings.build_type"]',
source_dir])

do_run(['cmake', '--preset', preset_name, '-DBUILD_TESTING=ON', f'-DBUILD_SHARED_LIBS={shared}',
f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={build_dir}'])

do_run(['cmake', '--build', '--preset', preset_name, '--config', preset])

do_run(['ctest', '--test-dir', build_dir, '-C', preset, '--output-on-failure'])


if __name__ == "__main__":
main()
17 changes: 3 additions & 14 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,6 @@ jobs:
conan --version
conan profile detect
- name: Install dependencies
run: |
conan install -b missing -s compiler.cppstd=17 -s build_type=${{ matrix.build_type }} -c:h tools.cmake.cmake_layout:build_folder_vars='[]' .
- name: Build library
run: |
cmake -H. -Bbuild/${{ matrix.build_type }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DBUILD_TESTING=ON \
-DCMAKE_TOOLCHAIN_FILE=./build/${{ matrix.build_type }}/generators/conan_toolchain.cmake \
-DBUILD_SHARED_LIBS="${{ matrix.shared }}"
cmake --build build/${{ matrix.build_type }}
- name: Run Tests
run: |
ctest --test-dir build/${{ matrix.build_type }}
- name: Build and test
run: |
python3 .ci/pr.py ${{ matrix.build_type }} ${{ matrix.shared }}
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ add_library(library
src/errors/sqlite.cpp
src/connection.cpp
src/statement.cpp
src/transaction.cpp
src/versioned_database.cpp
)

Expand Down
5 changes: 4 additions & 1 deletion include/sqlite-burrito/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <sqlite-burrito/config.h>
#include <sqlite-burrito/export.h>
#include <sqlite-burrito/transaction.h>

#include <sqlite3.h>

Expand Down Expand Up @@ -86,12 +87,14 @@ class SQLITE_BURRITO_EXPORT connection {
public:
[[nodiscard]] std::int64_t last_insert_rowid();


[[nodiscard]] std::error_code last_error() const noexcept;

[[nodiscard]] auto &native_handle() noexcept { return *connection_; };
[[nodiscard]] const auto &native_handle() const noexcept { return *connection_; }

[[nodiscard]] transaction begin_transaction(
transaction::behavior behavior = transaction::behavior::default_behavior);

private:
//! Database open flags
open_flags flags_;
Expand Down
1 change: 1 addition & 0 deletions include/sqlite-burrito/statement.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <sqlite-burrito/config.h>
#include <sqlite-burrito/errors/sqlite.h>
#include <sqlite-burrito/export.h>
#include <sqlite-burrito/connection.h>

#include <array>
#include <functional>
Expand Down
56 changes: 56 additions & 0 deletions include/sqlite-burrito/transaction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @file transaction.h
* @author Dennis Sitelew
* @date May 19, 2023
*/

#ifndef INCLUDE_SQLITE_BURRITO_TRANSACTION_H
#define INCLUDE_SQLITE_BURRITO_TRANSACTION_H

#include <sqlite-burrito/export.h>

#include <system_error>

namespace sqlite_burrito {

class connection;

class SQLITE_BURRITO_EXPORT transaction {
public:
//! Transaction behavior, see https://www.sqlite.org/lang_transaction.html for more details
enum class behavior {
deferred,
immediate,
exclusive,
default_behavior = deferred,
};

public:
transaction(connection &con, behavior behavior = behavior::default_behavior);

transaction(transaction &) = delete;
transaction(transaction &&) = default;

~transaction();

public:
transaction &operator=(transaction &) = delete;
transaction &operator=(transaction &&) = default;

public:
void commit();
void commit(std::error_code &ec) noexcept;

void rollback();
void rollback(std::error_code &ec) noexcept;

private:
connection *con_;

bool committed_{false};
bool rolled_back_{false};
};

} // namespace sqlite_burrito

#endif // INCLUDE_SQLITE_BURRITO_TRANSACTION_H
4 changes: 4 additions & 0 deletions src/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ std::int64_t connection::last_insert_rowid() {
std::error_code connection::last_error() const noexcept {
return errors::make_error_code(sqlite3_extended_errcode(connection_));
}

transaction connection::begin_transaction(transaction::behavior behavior) {
return transaction{*this, behavior};
}
2 changes: 1 addition & 1 deletion src/statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ statement::iterator_t statement::prepare(std::string_view text, std::error_code

auto distance = std::distance(text.data(), tail);

if (distance < text.length()) {
if (distance < static_cast<decltype(distance)>(text.length())) {
return text.begin() + distance;
} else {
return std::end(text);
Expand Down
78 changes: 78 additions & 0 deletions src/transaction.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @file transaction.cpp
* @author Dennis Sitelew
* @date May 19, 2023
*/

#include <sqlite-burrito/transaction.h>
#include <sqlite-burrito/statement.h>

using namespace sqlite_burrito;

transaction::transaction(connection &con, behavior behavior)
: con_{&con} {
const char *stmt;
switch (behavior) {
case behavior::deferred:
stmt = "BEGIN DEFERRED";
break;

case behavior::immediate:
stmt = "BEGIN IMMEDIATE";
break;

case behavior::exclusive:
stmt = "BEGIN EXCLUSIVE";
break;

default:
throw std::system_error(std::make_error_code(std::errc::invalid_argument));
}

statement::execute(*con_, stmt);
}

transaction::~transaction() {
if (rolled_back_ || committed_) {
return;
}

// TODO: Handle rollback errors to avoid throwing from the destructor?
statement::execute(*con_, "ROLLBACK TRANSACTION");
}

void transaction::commit() {
std::error_code ec;
commit(ec);
if (ec) {
throw std::system_error(ec);
}
}

void transaction::commit(std::error_code &ec) noexcept {
if (committed_ || rolled_back_) {
ec = std::make_error_code(std::errc::invalid_argument);
return;
}

statement::execute(*con_, "COMMIT TRANSACTION", ec);
committed_ = true;
}

void transaction::rollback() {
std::error_code ec;
rollback(ec);
if (ec) {
throw std::system_error(ec);
}
}

void transaction::rollback(std::error_code &ec) noexcept {
if (committed_ || rolled_back_) {
ec = std::make_error_code(std::errc::invalid_argument);
return;
}

statement::execute(*con_, "ROLLBACK TRANSACTION", ec);
rolled_back_ = true;
}
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ add_executable(main
src/errors/sqlite.cpp
src/connection.cpp
src/statement.cpp
src/transaction.cpp
src/versioned_database.cpp
)

Expand Down
119 changes: 119 additions & 0 deletions test/src/transaction.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @file transaction.cpp
* @author Dennis Sitelew
* @date May 19, 2023
*/

#include <catch2/catch.hpp>

#include <sqlite-burrito/connection.h>
#include <sqlite-burrito/statement.h>
#include <sqlite-burrito/transaction.h>

#include <system_error>

using namespace sqlite_burrito;

namespace {

class transaction_test {
public:
transaction_test() {
con_.open(":memory:");
statement::execute(con_, "CREATE TABLE test(value TEXT);");
select_.prepare("SELECT COUNT(*) FROM test;");
insert_.prepare("INSERT INTO test(value) VALUES (:pvalue);");
}

public:
int count_entries() {
int result;
select_.reset();
select_.step();
select_.get(0, result);
return result;
}

void add_entry(const char *entry) {
insert_.reset();
insert_.bind(":pvalue", entry);
insert_.execute();
}

protected:
connection con_{};
statement select_{con_};
statement insert_{con_};
};

} // namespace

TEST_CASE_METHOD(transaction_test, "Commit should propagate changes", "[transaction]") {
REQUIRE(count_entries() == 0);

auto trans = con_.begin_transaction();
REQUIRE_NOTHROW(add_entry("test"));
REQUIRE_NOTHROW(trans.commit());
REQUIRE(count_entries() == 1);

// Already committed
REQUIRE_THROWS_AS(trans.commit(), std::system_error);
REQUIRE_THROWS_AS(trans.rollback(), std::system_error);
}

TEST_CASE_METHOD(transaction_test, "Rollback should discard changes", "[transaction]") {
REQUIRE(count_entries() == 0);

transaction trans{con_};
REQUIRE_NOTHROW(add_entry("test"));
REQUIRE_NOTHROW(trans.rollback());
REQUIRE(count_entries() == 0);

// Already committed
REQUIRE_THROWS_AS(trans.commit(), std::system_error);
REQUIRE_THROWS_AS(trans.rollback(), std::system_error);
}

TEST_CASE_METHOD(transaction_test, "Auto-rollback on scope exit", "[transaction]") {
REQUIRE(count_entries() == 0);
{
transaction trans{con_};
REQUIRE_NOTHROW(add_entry("test"));
}
REQUIRE(count_entries() == 0);
}

TEST_CASE_METHOD(transaction_test, "Manual-rollback should not result in errors on scope exit", "[transaction]") {
REQUIRE(count_entries() == 0);
{
transaction trans{con_};
REQUIRE_NOTHROW(add_entry("test"));
trans.rollback();
}
REQUIRE(count_entries() == 0);
}

TEST_CASE_METHOD(transaction_test, "Manual-commit should not result in errors on scope exit", "[transaction]") {
REQUIRE(count_entries() == 0);
{
transaction trans{con_};
REQUIRE_NOTHROW(add_entry("test"));
trans.commit();
}
REQUIRE(count_entries() == 1);
}

TEST_CASE_METHOD(transaction_test, "Changes after rolling back should be propagated", "[transaction]") {
REQUIRE(count_entries() == 0);

{
transaction trans{con_};
REQUIRE_NOTHROW(add_entry("1"));
trans.rollback();

REQUIRE_NOTHROW(add_entry("2"));
REQUIRE_NOTHROW(add_entry("3"));
}

REQUIRE(count_entries() == 2);
}

0 comments on commit 71e767d

Please sign in to comment.