Formatting
publish / publish (push) Successful in 16m36s

This commit is contained in:
khodowany
2026-09-14 23:55:04 -05:00
parent 16c37ab1c5
commit 46f7c13121
23 changed files with 391 additions and 454 deletions
+4
View File
@@ -48,6 +48,10 @@ jobs:
restore-keys: conan-${{ runner.os }}- restore-keys: conan-${{ runner.os }}-
- name: Configure native release - name: Configure native release
run: python3 modulus.py configure --target x86_64_gcc12_release run: python3 modulus.py configure --target x86_64_gcc12_release
- name: Check code style
run: |
python3 -m pip install --no-cache-dir -r build/build_tools/docker/format-requirements.txt
python3 modulus.py format --check
- name: Build native release - name: Build native release
run: python3 modulus.py build --target x86_64_gcc12_release run: python3 modulus.py build --target x86_64_gcc12_release
- name: Test native release - name: Test native release
+2
View File
@@ -6,6 +6,8 @@ Noto uses Modulus for shared build tooling, Boost.JSON, and SQLite integration.
## Build ## Build
Code style is managed by Modulus: run `./modulus.py format` or `./modulus.py format --check`. The generated Neovim integration uses the same pinned container formatter for C++ and Python, including existing format-on-save toggles. Configure first to prepare tooling; customize `tooling.editor.clang_format` and `tooling.format.exclude` in `modulus.json`, not generated editor files. See the shared Modulus README for the compact layout and no-alias policy.
Configure, build, and test the default target: Configure, build, and test the default target:
```sh ```sh
-8
View File
@@ -41,15 +41,12 @@ class noto_conan(conan.ConanFile):
"tests/*.cpp", "tests/*.cpp",
"tests/*.hpp", "tests/*.hpp",
) )
def config_options(self): def config_options(self):
if self.settings.os == "Windows": if self.settings.os == "Windows":
del self.options.fPIC del self.options.fPIC
def configure(self): def configure(self):
if self.options.shared: if self.options.shared:
del self.options.fPIC del self.options.fPIC
def requirements(self): def requirements(self):
self.requires( self.requires(
"modulus/latest", "modulus/latest",
@@ -59,7 +56,6 @@ class noto_conan(conan.ConanFile):
"with_sqlite": True, "with_sqlite": True,
}, },
) )
def generate(self): def generate(self):
deps = conan.tools.cmake.CMakeDeps(self) deps = conan.tools.cmake.CMakeDeps(self)
deps.generate() deps.generate()
@@ -69,14 +65,12 @@ class noto_conan(conan.ConanFile):
toolchain.variables["BUILD_SHARED_LIBS"] = bool(self.options.shared) toolchain.variables["BUILD_SHARED_LIBS"] = bool(self.options.shared)
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests) toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
toolchain.generate() toolchain.generate()
def build(self): def build(self):
cmake = conan.tools.cmake.CMake(self) cmake = conan.tools.cmake.CMake(self)
cmake.configure() cmake.configure()
cmake.build() cmake.build()
if self.options.with_tests and conan.tools.build.can_run(self): if self.options.with_tests and conan.tools.build.can_run(self):
cmake.test(target="check") cmake.test(target="check")
def package_id(self): def package_id(self):
self.info.options.rm_safe("with_tests") self.info.options.rm_safe("with_tests")
for requirement in ( for requirement in (
@@ -88,11 +82,9 @@ class noto_conan(conan.ConanFile):
): ):
if requirement in self.info.requires.pkg_names: if requirement in self.info.requires.pkg_names:
self.info.requires.remove(requirement) self.info.requires.remove(requirement)
def package(self): def package(self):
cmake = conan.tools.cmake.CMake(self) cmake = conan.tools.cmake.CMake(self)
cmake.install() cmake.install()
def package_info(self): def package_info(self):
self.cpp_info.set_property( self.cpp_info.set_property(
"cmake_file_name", "cmake_file_name",
+7 -7
View File
@@ -35,14 +35,14 @@ struct connection_config {
std::vector<noto::pragma_setting> pragmas{}; std::vector<noto::pragma_setting> pragmas{};
}; };
auto validate(noto::connection_config const& config) -> std::expected<void, modulus::error>; auto validate(noto::connection_config const &config) -> std::expected<void, modulus::error>;
auto memory_connection_config(std::string name = {}) -> noto::connection_config; auto memory_connection_config(std::string name = {}) -> noto::connection_config;
auto file_connection_config(std::string filename, std::string name = {}) -> noto::connection_config; auto file_connection_config(std::string filename, std::string name = {}) -> noto::connection_config;
auto parse_database_config(boost::json::value const& value) -> std::expected<noto::connection_config, modulus::error>; auto parse_database_config(boost::json::value const &value) -> std::expected<noto::connection_config, modulus::error>;
auto parse_database_configs(boost::json::value const& value) -> std::expected<std::vector<noto::connection_config>, modulus::error>; auto parse_database_configs(boost::json::value const &value) -> std::expected<std::vector<noto::connection_config>, modulus::error>;
auto load_database_configs(std::string const& path) -> std::expected<std::vector<noto::connection_config>, modulus::error>; auto load_database_configs(std::string const &path) -> std::expected<std::vector<noto::connection_config>, modulus::error>;
auto to_json(std::variant<std::bool_t, std::int64_t, std::string> const& value) -> boost::json::value; auto to_json(std::variant<std::bool_t, std::int64_t, std::string> const &value) -> boost::json::value;
auto to_json(noto::connection_config const& config) -> boost::json::value; auto to_json(noto::connection_config const &config) -> boost::json::value;
auto to_json(std::vector<noto::connection_config> const& configs) -> boost::json::value; auto to_json(std::vector<noto::connection_config> const &configs) -> boost::json::value;
} }
+11 -10
View File
@@ -28,6 +28,7 @@ namespace noto {
namespace detail { namespace detail {
struct statement_core; struct statement_core;
struct connection_core; struct connection_core;
} }
@@ -46,7 +47,7 @@ struct sqlite_value {
sqlite_value(std::int32_t value); sqlite_value(std::int32_t value);
sqlite_value(std::int64_t value); sqlite_value(std::int64_t value);
sqlite_value(std::float64_t value); sqlite_value(std::float64_t value);
sqlite_value(std::char_t const* value); sqlite_value(std::char_t const *value);
sqlite_value(std::string value); sqlite_value(std::string value);
sqlite_value(std::string_view value); sqlite_value(std::string_view value);
sqlite_value(std::vector<std::byte> value); sqlite_value(std::vector<std::byte> value);
@@ -84,11 +85,11 @@ class connection;
class statement { class statement {
public: public:
statement() = default; statement() = default;
statement(noto::statement const&) = delete; statement(noto::statement const &) = delete;
statement(noto::statement&& other) noexcept = default; statement(noto::statement &&other) noexcept = default;
~statement(); ~statement();
auto operator=(noto::statement const&) -> noto::statement& = delete; auto operator=(noto::statement const &) -> noto::statement & = delete;
auto operator=(noto::statement&& other) noexcept -> noto::statement& = default; auto operator=(noto::statement &&other) noexcept -> noto::statement & = default;
auto valid() const -> std::bool_t; auto valid() const -> std::bool_t;
auto async_reset(std::vector<noto::sqlite_value> params = {}) -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_reset(std::vector<noto::sqlite_value> params = {}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>>; auto async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>>;
@@ -105,11 +106,11 @@ private:
class connection { class connection {
public: public:
connection(boost::asio::any_io_executor executor, noto::connection_config config); connection(boost::asio::any_io_executor executor, noto::connection_config config);
connection(noto::connection const&) = delete; connection(noto::connection const &) = delete;
connection(noto::connection&&) noexcept = default; connection(noto::connection &&) noexcept = default;
~connection(); ~connection();
auto operator=(noto::connection const&) -> noto::connection& = delete; auto operator=(noto::connection const &) -> noto::connection & = delete;
auto operator=(noto::connection&&) noexcept -> noto::connection& = default; auto operator=(noto::connection &&) noexcept -> noto::connection & = default;
auto config() const -> noto::connection_config; auto config() const -> noto::connection_config;
auto state() const -> noto::connection_state; auto state() const -> noto::connection_state;
auto async_connect() -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_connect() -> boost::asio::awaitable<std::expected<void, modulus::error>>;
@@ -168,7 +169,7 @@ auto noto::connection::async_with_transaction(noto::transaction_mode mode, CALLB
auto exception_status = std::optional<modulus::error>{}; auto exception_status = std::optional<modulus::error>{};
try { try {
callback_status = co_await callback(connection); callback_status = co_await callback(connection);
} catch (std::exception const& error) { } catch (std::exception const &error) {
exception_status = noto::detail::make_error(noto::detail::error_internal, std::string{"transaction callback threw: "} + error.what()); exception_status = noto::detail::make_error(noto::detail::error_internal, std::string{"transaction callback threw: "} + error.what());
} catch (...) { } catch (...) {
exception_status = noto::detail::make_error(noto::detail::error_internal, "transaction callback threw an unknown exception"); exception_status = noto::detail::make_error(noto::detail::error_internal, "transaction callback threw an unknown exception");
+3 -3
View File
@@ -51,7 +51,7 @@ struct connection_core : std::enable_shared_from_this<noto::detail::connection_c
boost::asio::strand<boost::asio::any_io_executor> strand; boost::asio::strand<boost::asio::any_io_executor> strand;
noto::connection_config config{}; noto::connection_config config{};
std::chrono::milliseconds busy_timeout{}; std::chrono::milliseconds busy_timeout{};
sqlite3* database{}; sqlite3 *database{};
std::shared_ptr<std::atomic<noto::connection_state>> state{std::make_shared<std::atomic<noto::connection_state>>(noto::connection_state::IDLE)}; std::shared_ptr<std::atomic<noto::connection_state>> state{std::make_shared<std::atomic<noto::connection_state>>(noto::connection_state::IDLE)};
std::bool_t busy{}; std::bool_t busy{};
std::bool_t transaction_active{}; std::bool_t transaction_active{};
@@ -59,14 +59,14 @@ struct connection_core : std::enable_shared_from_this<noto::detail::connection_c
}; };
struct statement_core { struct statement_core {
statement_core(std::shared_ptr<noto::detail::connection_core> connection, sqlite3_stmt* statement); statement_core(std::shared_ptr<noto::detail::connection_core> connection, sqlite3_stmt *statement);
~statement_core(); ~statement_core();
auto async_reset(std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_reset(std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>>; auto async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>>;
auto async_execute(std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>>; auto async_execute(std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>>;
auto async_finalize() -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_finalize() -> boost::asio::awaitable<std::expected<void, modulus::error>>;
std::shared_ptr<noto::detail::connection_core> connection{}; std::shared_ptr<noto::detail::connection_core> connection{};
sqlite3_stmt* statement{}; sqlite3_stmt *statement{};
std::atomic_bool finalized{}; std::atomic_bool finalized{};
std::bool_t counted{}; std::bool_t counted{};
}; };
+4 -3
View File
@@ -8,6 +8,7 @@
#include <modulus.hpp> #include <modulus.hpp>
namespace noto::detail { namespace noto::detail {
constexpr std::int32_t error_invalid_argument{1}; constexpr std::int32_t error_invalid_argument{1};
@@ -29,13 +30,13 @@ constexpr std::int32_t error_internal{15};
auto make_error(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error; auto make_error(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
auto fail_void(modulus::error error) -> std::expected<void, modulus::error>; auto fail_void(modulus::error error) -> std::expected<void, modulus::error>;
auto fail_void(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<void, modulus::error>; auto fail_void(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<void, modulus::error>;
auto fail_void(std::expected<void, modulus::error> const& result) -> std::expected<void, modulus::error>; auto fail_void(std::expected<void, modulus::error> const &result) -> std::expected<void, modulus::error>;
template <typename VALUE> template <typename VALUE>
auto fail(modulus::error error) -> std::expected<VALUE, modulus::error>; auto fail(modulus::error error) -> std::expected<VALUE, modulus::error>;
template <typename VALUE> template <typename VALUE>
auto fail(std::expected<void, modulus::error> const& result) -> std::expected<VALUE, modulus::error>; auto fail(std::expected<void, modulus::error> const &result) -> std::expected<VALUE, modulus::error>;
template <typename VALUE> template <typename VALUE>
auto fail(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<VALUE, modulus::error>; auto fail(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<VALUE, modulus::error>;
@@ -48,7 +49,7 @@ auto noto::detail::fail(modulus::error error) -> std::expected<VALUE, modulus::e
} }
template <typename VALUE> template <typename VALUE>
auto noto::detail::fail(std::expected<void, modulus::error> const& result) -> std::expected<VALUE, modulus::error> { auto noto::detail::fail(std::expected<void, modulus::error> const &result) -> std::expected<VALUE, modulus::error> {
assert(!result); assert(!result);
return modulus::fail<VALUE>(result.error()); return modulus::fail<VALUE>(result.error());
} }
+1
View File
@@ -6,6 +6,7 @@
#include <modulus.hpp> #include <modulus.hpp>
namespace noto::detail { namespace noto::detail {
auto quote_sql_literal(std::string_view value, std::string_view label, std::bool_t allow_empty = true) -> std::expected<std::string, modulus::error>; auto quote_sql_literal(std::string_view value, std::string_view label, std::bool_t allow_empty = true) -> std::expected<std::string, modulus::error>;
+3 -2
View File
@@ -6,11 +6,12 @@
#include <modulus.hpp> #include <modulus.hpp>
#include <cstdint> #include <cstdint>
struct sqlite3; struct sqlite3;
namespace noto { namespace noto {
auto error_from(std::int32_t code, sqlite3* database = nullptr, std::string_view operation = {}) -> modulus::error; auto error_from(std::int32_t code, sqlite3 *database = nullptr, std::string_view operation = {}) -> modulus::error;
auto result_from(std::int32_t code, sqlite3* database = nullptr, std::string_view operation = {}) -> std::expected<void, modulus::error>; auto result_from(std::int32_t code, sqlite3 *database = nullptr, std::string_view operation = {}) -> std::expected<void, modulus::error>;
} }
+4 -4
View File
@@ -18,8 +18,8 @@ struct migration {
std::string id{}; std::string id{};
std::string description{}; std::string description{};
std::string checksum{}; std::string checksum{};
std::function<boost::asio::awaitable<std::expected<void, modulus::error>>(noto::connection&)> up{}; std::function<boost::asio::awaitable<std::expected<void, modulus::error>>(noto::connection &)> up{};
std::function<boost::asio::awaitable<std::expected<void, modulus::error>>(noto::connection&)> down{}; std::function<boost::asio::awaitable<std::expected<void, modulus::error>>(noto::connection &)> down{};
}; };
struct migration_record { struct migration_record {
@@ -35,12 +35,12 @@ struct migration_options {
class migrator { class migrator {
public: public:
migrator(noto::connection& connection, noto::migration_options options = {}); migrator(noto::connection &connection, noto::migration_options options = {});
auto async_applied_migrations() -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>>; auto async_applied_migrations() -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>>;
auto async_migrate(std::vector<noto::migration> migrations) -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_migrate(std::vector<noto::migration> migrations) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto async_rollback(std::vector<noto::migration> migrations, std::string target_id = {}) -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_rollback(std::vector<noto::migration> migrations, std::string target_id = {}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
private: private:
noto::connection* connection_{}; noto::connection *connection_{};
noto::migration_options options_{}; noto::migration_options options_{};
}; };
+8 -7
View File
@@ -9,13 +9,14 @@
#include <modulus.hpp> #include <modulus.hpp>
namespace noto { namespace noto {
class trusted_sql_fragment { class trusted_sql_fragment {
public: public:
trusted_sql_fragment() = default; trusted_sql_fragment() = default;
static auto unsafe(std::string sql) -> std::expected<noto::trusted_sql_fragment, modulus::error>; static auto unsafe(std::string sql) -> std::expected<noto::trusted_sql_fragment, modulus::error>;
auto value() const -> std::string const&; auto value() const -> std::string const &;
auto empty() const -> std::bool_t; auto empty() const -> std::bool_t;
private: private:
explicit trusted_sql_fragment(std::string sql); explicit trusted_sql_fragment(std::string sql);
@@ -35,7 +36,7 @@ public:
static auto timestamp() -> noto::column_type; static auto timestamp() -> noto::column_type;
static auto decimal(std::uint32_t precision, std::uint32_t scale) -> std::expected<noto::column_type, modulus::error>; static auto decimal(std::uint32_t precision, std::uint32_t scale) -> std::expected<noto::column_type, modulus::error>;
static auto trusted(noto::trusted_sql_fragment fragment) -> noto::column_type; static auto trusted(noto::trusted_sql_fragment fragment) -> noto::column_type;
auto sql() const -> std::string const&; auto sql() const -> std::string const &;
auto empty() const -> std::bool_t; auto empty() const -> std::bool_t;
private: private:
explicit column_type(std::string sql); explicit column_type(std::string sql);
@@ -53,7 +54,7 @@ public:
static auto boolean(std::bool_t value) -> noto::default_value; static auto boolean(std::bool_t value) -> noto::default_value;
static auto current_timestamp() -> noto::default_value; static auto current_timestamp() -> noto::default_value;
static auto trusted_expression(noto::trusted_sql_fragment fragment) -> noto::default_value; static auto trusted_expression(noto::trusted_sql_fragment fragment) -> noto::default_value;
auto sql() const -> std::string const&; auto sql() const -> std::string const &;
auto empty() const -> std::bool_t; auto empty() const -> std::bool_t;
private: private:
explicit default_value(std::string sql); explicit default_value(std::string sql);
@@ -137,10 +138,10 @@ struct index_definition {
}; };
auto quote_identifier(std::string_view identifier) -> std::expected<std::string, modulus::error>; auto quote_identifier(std::string_view identifier) -> std::expected<std::string, modulus::error>;
auto create_table_sql(noto::table_definition const& definition) -> std::expected<std::string, modulus::error>; auto create_table_sql(noto::table_definition const &definition) -> std::expected<std::string, modulus::error>;
auto drop_table_sql(noto::table_name const& table, std::bool_t if_exists = true) -> std::expected<std::string, modulus::error>; auto drop_table_sql(noto::table_name const &table, std::bool_t if_exists = true) -> std::expected<std::string, modulus::error>;
auto create_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error>; auto create_index_sql(noto::index_definition const &definition) -> std::expected<std::string, modulus::error>;
auto drop_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error>; auto drop_index_sql(noto::index_definition const &definition) -> std::expected<std::string, modulus::error>;
auto table_exists_sql() -> std::string; auto table_exists_sql() -> std::string;
} }
+5 -4
View File
@@ -13,6 +13,7 @@ namespace noto {
namespace detail { namespace detail {
struct connection_core; struct connection_core;
struct transaction_state; struct transaction_state;
} }
@@ -28,11 +29,11 @@ class connection;
class transaction { class transaction {
public: public:
transaction() = default; transaction() = default;
transaction(noto::transaction const&) = delete; transaction(noto::transaction const &) = delete;
transaction(noto::transaction&& other) noexcept; transaction(noto::transaction &&other) noexcept;
~transaction(); ~transaction();
auto operator=(noto::transaction const&) -> noto::transaction& = delete; auto operator=(noto::transaction const &) -> noto::transaction & = delete;
auto operator=(noto::transaction&& other) noexcept -> noto::transaction& = delete; auto operator=(noto::transaction &&other) noexcept -> noto::transaction & = delete;
auto active() const -> std::bool_t; auto active() const -> std::bool_t;
auto async_commit() -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_commit() -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto async_rollback() -> boost::asio::awaitable<std::expected<void, modulus::error>>; auto async_rollback() -> boost::asio::awaitable<std::expected<void, modulus::error>>;
+6 -2
View File
@@ -16,6 +16,7 @@ COMMANDS = (
"package", "package",
"lsp", "lsp",
"clangd", "clangd",
"format",
) )
COMMON_DIR = pathlib.Path("build") / "build_common" COMMON_DIR = pathlib.Path("build") / "build_common"
TOOLS_DIR = pathlib.Path("build") / "build_tools" TOOLS_DIR = pathlib.Path("build") / "build_tools"
@@ -60,6 +61,9 @@ def runtime_ready(root):
and (root / TOOLS_DIR / "clangd.py").is_file() and (root / TOOLS_DIR / "clangd.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file() and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file() and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
and (root / TOOLS_DIR / "format.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "formatting.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "format_layout.py").is_file()
) )
def optional_credentials(): def optional_credentials():
@@ -340,9 +344,9 @@ def ensure_runtime(root):
def delegate(command, argv): def delegate(command, argv):
root = repo_root() root = repo_root()
if command == "clangd": if command in {"clangd", "format"}:
if not runtime_ready(root): if not runtime_ready(root):
print("Modulus clangd tooling is missing; run ./modulus.py configure first", file=sys.stderr) print(f"Modulus {command} tooling is missing; run ./modulus.py configure first", file=sys.stderr)
return 1 return 1
else: else:
ensure_runtime(root) ensure_runtime(root)
+25 -22
View File
@@ -19,13 +19,13 @@
#include <noto/detail/errors.hpp> #include <noto/detail/errors.hpp>
namespace { namespace {
constexpr auto json_options = modulus::json_config::options{ constexpr auto json_options = modulus::json_config::options{
.category = "noto", .category = "noto",
.invalid_argument_code = noto::detail::error_invalid_argument, .invalid_argument_code = noto::detail::error_invalid_argument,
.not_found_code = noto::detail::error_not_found .not_found_code = noto::detail::error_not_found};
};
auto invalid(std::string message) -> std::expected<void, modulus::error> { auto invalid(std::string message) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message)); return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message));
@@ -72,7 +72,7 @@ auto mode_name(noto::open_mode mode) -> std::string_view {
return "read_write_create"; return "read_write_create";
} }
auto required_string_field(boost::json::object const& object, std::string_view name) -> std::expected<std::string, modulus::error> { auto required_string_field(boost::json::object const &object, std::string_view name) -> std::expected<std::string, modulus::error> {
auto value = modulus::json_config::required_string_field(object, name, "database", json_options); auto value = modulus::json_config::required_string_field(object, name, "database", json_options);
if (!value) { if (!value) {
return noto::detail::fail<std::string>(value.error()); return noto::detail::fail<std::string>(value.error());
@@ -83,7 +83,7 @@ auto required_string_field(boost::json::object const& object, std::string_view n
return std::move(*value); return std::move(*value);
} }
auto optional_string_field(boost::json::object const& object, std::string_view name) -> std::expected<std::optional<std::string>, modulus::error> { auto optional_string_field(boost::json::object const &object, std::string_view name) -> std::expected<std::optional<std::string>, modulus::error> {
auto found = object.find(name); auto found = object.find(name);
if (found == object.end()) { if (found == object.end()) {
return std::optional<std::string>{}; return std::optional<std::string>{};
@@ -102,7 +102,7 @@ auto optional_string_field(boost::json::object const& object, std::string_view n
return std::optional<std::string>{std::move(value)}; return std::optional<std::string>{std::move(value)};
} }
auto parse_pragma_value(boost::json::value const& value, std::string_view name) -> std::expected<std::variant<std::bool_t, std::int64_t, std::string>, modulus::error> { auto parse_pragma_value(boost::json::value const &value, std::string_view name) -> std::expected<std::variant<std::bool_t, std::int64_t, std::string>, modulus::error> {
if (value.is_bool()) { if (value.is_bool()) {
return std::variant<std::bool_t, std::int64_t, std::string>{value.as_bool()}; return std::variant<std::bool_t, std::int64_t, std::string>{value.as_bool()};
} }
@@ -123,7 +123,7 @@ auto parse_pragma_value(boost::json::value const& value, std::string_view name)
return invalid_result<std::variant<std::bool_t, std::int64_t, std::string>>(std::string{name} + " pragma value must be a string, integer, or boolean"); return invalid_result<std::variant<std::bool_t, std::int64_t, std::string>>(std::string{name} + " pragma value must be a string, integer, or boolean");
} }
auto parse_pragmas(boost::json::object const& object) -> std::expected<std::vector<noto::pragma_setting>, modulus::error> { auto parse_pragmas(boost::json::object const &object) -> std::expected<std::vector<noto::pragma_setting>, modulus::error> {
auto found = object.find("pragmas"); auto found = object.find("pragmas");
if (found == object.end()) { if (found == object.end()) {
return std::vector<noto::pragma_setting>{}; return std::vector<noto::pragma_setting>{};
@@ -133,7 +133,7 @@ auto parse_pragmas(boost::json::object const& object) -> std::expected<std::vect
return noto::detail::fail<std::vector<noto::pragma_setting>>(pragma_object.error()); return noto::detail::fail<std::vector<noto::pragma_setting>>(pragma_object.error());
} }
auto pragmas = std::vector<noto::pragma_setting>{}; auto pragmas = std::vector<noto::pragma_setting>{};
for (auto const& item : **pragma_object) { for (auto const &item : **pragma_object) {
auto name = std::string{item.key()}; auto name = std::string{item.key()};
if (!valid_pragma_name(name)) { if (!valid_pragma_name(name)) {
return invalid_result<std::vector<noto::pragma_setting>>("database pragma names must contain only letters, digits, or underscores"); return invalid_result<std::vector<noto::pragma_setting>>("database pragma names must contain only letters, digits, or underscores");
@@ -152,7 +152,7 @@ auto parse_pragmas(boost::json::object const& object) -> std::expected<std::vect
} }
auto noto::validate(noto::connection_config const& config) -> std::expected<void, modulus::error> { auto noto::validate(noto::connection_config const &config) -> std::expected<void, modulus::error> {
if (config.filename.empty()) { if (config.filename.empty()) {
return invalid("database filename must not be empty"); return invalid("database filename must not be empty");
} }
@@ -168,7 +168,7 @@ auto noto::validate(noto::connection_config const& config) -> std::expected<void
if (static_cast<std::uint64_t>(config.busy_timeout.count()) > sqlite_int_max()) { if (static_cast<std::uint64_t>(config.busy_timeout.count()) > sqlite_int_max()) {
return invalid("database busy timeout is out of range"); return invalid("database busy timeout is out of range");
} }
for (auto const& pragma : config.pragmas) { for (auto const &pragma : config.pragmas) {
if (!valid_pragma_name(pragma.name)) { if (!valid_pragma_name(pragma.name)) {
return invalid("database pragma names must contain only letters, digits, or underscores"); return invalid("database pragma names must contain only letters, digits, or underscores");
} }
@@ -191,7 +191,7 @@ auto noto::file_connection_config(std::string filename, std::string name) -> not
}; };
} }
auto noto::parse_database_config(boost::json::value const& value) -> std::expected<noto::connection_config, modulus::error> { auto noto::parse_database_config(boost::json::value const &value) -> std::expected<noto::connection_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "database", json_options); auto object = modulus::json_config::require_object(value, "database", json_options);
if (!object) { if (!object) {
return noto::detail::fail<noto::connection_config>(object.error()); return noto::detail::fail<noto::connection_config>(object.error());
@@ -204,7 +204,8 @@ auto noto::parse_database_config(boost::json::value const& value) -> std::expect
"busy_timeout_ms", "busy_timeout_ms",
"foreign_keys", "foreign_keys",
"pragmas", "pragmas",
}, "database", json_options); },
"database", json_options);
if (!unknown) { if (!unknown) {
return noto::detail::fail<noto::connection_config>(unknown.error()); return noto::detail::fail<noto::connection_config>(unknown.error());
} }
@@ -229,7 +230,8 @@ auto noto::parse_database_config(boost::json::value const& value) -> std::expect
.name = "memory", .name = "memory",
.value = noto::open_mode::MEMORY, .value = noto::open_mode::MEMORY,
}, },
}, "database mode must be read_only, read_write, read_write_create, or memory", json_options); },
"database mode must be read_only, read_write, read_write_create, or memory", json_options);
if (!mode) { if (!mode) {
return noto::detail::fail<noto::connection_config>(mode.error()); return noto::detail::fail<noto::connection_config>(mode.error());
} }
@@ -272,7 +274,7 @@ auto noto::parse_database_config(boost::json::value const& value) -> std::expect
return config; return config;
} }
auto noto::parse_database_configs(boost::json::value const& value) -> std::expected<std::vector<noto::connection_config>, modulus::error> { auto noto::parse_database_configs(boost::json::value const &value) -> std::expected<std::vector<noto::connection_config>, modulus::error> {
auto object = modulus::json_config::require_object(value, "config root", json_options); auto object = modulus::json_config::require_object(value, "config root", json_options);
if (!object) { if (!object) {
return noto::detail::fail<std::vector<noto::connection_config>>(object.error()); return noto::detail::fail<std::vector<noto::connection_config>>(object.error());
@@ -286,7 +288,7 @@ auto noto::parse_database_configs(boost::json::value const& value) -> std::expec
return invalid_result<std::vector<noto::connection_config>>("config databases array is required"); return invalid_result<std::vector<noto::connection_config>>("config databases array is required");
} }
auto configs = std::vector<noto::connection_config>{}; auto configs = std::vector<noto::connection_config>{};
for (auto const& database_value : found->value().as_array()) { for (auto const &database_value : found->value().as_array()) {
auto parsed = noto::parse_database_config(database_value); auto parsed = noto::parse_database_config(database_value);
if (!parsed) { if (!parsed) {
return noto::detail::fail<std::vector<noto::connection_config>>(parsed.error()); return noto::detail::fail<std::vector<noto::connection_config>>(parsed.error());
@@ -296,7 +298,7 @@ auto noto::parse_database_configs(boost::json::value const& value) -> std::expec
return configs; return configs;
} }
auto noto::load_database_configs(std::string const& path) -> std::expected<std::vector<noto::connection_config>, modulus::error> { auto noto::load_database_configs(std::string const &path) -> std::expected<std::vector<noto::connection_config>, modulus::error> {
auto value = modulus::json_config::load_value(path, json_options); auto value = modulus::json_config::load_value(path, json_options);
if (!value) { if (!value) {
return noto::detail::fail<std::vector<noto::connection_config>>(value.error()); return noto::detail::fail<std::vector<noto::connection_config>>(value.error());
@@ -304,15 +306,16 @@ auto noto::load_database_configs(std::string const& path) -> std::expected<std::
return noto::parse_database_configs(*value); return noto::parse_database_configs(*value);
} }
auto noto::to_json(std::variant<std::bool_t, std::int64_t, std::string> const& value) -> boost::json::value { auto noto::to_json(std::variant<std::bool_t, std::int64_t, std::string> const &value) -> boost::json::value {
return std::visit([](auto const& item) -> boost::json::value { return std::visit([](auto const &item) -> boost::json::value {
return boost::json::value{item}; return boost::json::value{item};
}, value); },
value);
} }
auto noto::to_json(noto::connection_config const& config) -> boost::json::value { auto noto::to_json(noto::connection_config const &config) -> boost::json::value {
auto pragmas = boost::json::object{}; auto pragmas = boost::json::object{};
for (auto const& pragma : config.pragmas) { for (auto const &pragma : config.pragmas) {
pragmas[pragma.name] = noto::to_json(pragma.value); pragmas[pragma.name] = noto::to_json(pragma.value);
} }
return boost::json::object{ return boost::json::object{
@@ -347,9 +350,9 @@ auto noto::to_json(noto::connection_config const& config) -> boost::json::value
}; };
} }
auto noto::to_json(std::vector<noto::connection_config> const& configs) -> boost::json::value { auto noto::to_json(std::vector<noto::connection_config> const &configs) -> boost::json::value {
auto databases = boost::json::array{}; auto databases = boost::json::array{};
for (auto const& config : configs) { for (auto const &config : configs) {
databases.push_back(noto::to_json(config)); databases.push_back(noto::to_json(config));
} }
return boost::json::object{ return boost::json::object{
+33 -20
View File
@@ -25,6 +25,7 @@
#include <noto/schema.hpp> #include <noto/schema.hpp>
#include <noto/transaction.hpp> #include <noto/transaction.hpp>
namespace { namespace {
auto invalid_connection() -> std::expected<void, modulus::error> { auto invalid_connection() -> std::expected<void, modulus::error> {
@@ -61,8 +62,8 @@ auto valid_token(std::string_view value) -> std::bool_t {
return true; return true;
} }
auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const& value) -> std::expected<std::string, modulus::error> { auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const &value) -> std::expected<std::string, modulus::error> {
return std::visit([](auto const& current) -> std::expected<std::string, modulus::error> { return std::visit([](auto const &current) -> std::expected<std::string, modulus::error> {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::bool_t>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::bool_t>) {
return std::string{current ? "1" : "0"}; return std::string{current ? "1" : "0"};
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) { } else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
@@ -70,25 +71,26 @@ auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const
} else { } else {
return noto::detail::quote_sql_literal(current, "pragma value"); return noto::detail::quote_sql_literal(current, "pragma value");
} }
}, value); },
value);
} }
auto first_int(noto::query_result const& result) -> std::expected<std::int64_t, modulus::error> { auto first_int(noto::query_result const &result) -> std::expected<std::int64_t, modulus::error> {
if (result.rows.empty() || result.rows.front().empty()) { if (result.rows.empty() || result.rows.front().empty()) {
return noto::detail::fail<std::int64_t>(noto::detail::error_not_found, "query returned no rows"); return noto::detail::fail<std::int64_t>(noto::detail::error_not_found, "query returned no rows");
} }
auto const& value = result.rows.front().front().value; auto const &value = result.rows.front().front().value;
if (auto integer = std::get_if<std::int64_t>(&value)) { if (auto integer = std::get_if<std::int64_t>(&value)) {
return *integer; return *integer;
} }
return noto::detail::fail<std::int64_t>(noto::detail::error_invalid_argument, "query did not return an integer"); return noto::detail::fail<std::int64_t>(noto::detail::error_invalid_argument, "query did not return an integer");
} }
auto first_string(noto::query_result const& result) -> std::expected<std::string, modulus::error> { auto first_string(noto::query_result const &result) -> std::expected<std::string, modulus::error> {
if (result.rows.empty() || result.rows.front().empty()) { if (result.rows.empty() || result.rows.front().empty()) {
return noto::detail::fail<std::string>(noto::detail::error_not_found, "query returned no rows"); return noto::detail::fail<std::string>(noto::detail::error_not_found, "query returned no rows");
} }
auto const& value = result.rows.front().front().value; auto const &value = result.rows.front().front().value;
if (auto text = std::get_if<std::string>(&value)) { if (auto text = std::get_if<std::string>(&value)) {
return *text; return *text;
} }
@@ -243,7 +245,7 @@ auto async_connection_integrity_check(noto::connection connection) -> boost::asi
co_return std::unexpected(result.error()); co_return std::unexpected(result.error());
} }
auto messages = std::vector<std::string>{}; auto messages = std::vector<std::string>{};
for (auto const& row : result->rows) { for (auto const &row : result->rows) {
if (!row.empty()) { if (!row.empty()) {
if (auto text = std::get_if<std::string>(&row.front().value)) { if (auto text = std::get_if<std::string>(&row.front().value)) {
messages.push_back(*text); messages.push_back(*text);
@@ -259,7 +261,7 @@ auto async_connection_quick_check(noto::connection connection) -> boost::asio::a
co_return std::unexpected(result.error()); co_return std::unexpected(result.error());
} }
auto messages = std::vector<std::string>{}; auto messages = std::vector<std::string>{};
for (auto const& row : result->rows) { for (auto const &row : result->rows) {
if (!row.empty()) { if (!row.empty()) {
if (auto text = std::get_if<std::string>(&row.front().value)) { if (auto text = std::get_if<std::string>(&row.front().value)) {
messages.push_back(*text); messages.push_back(*text);
@@ -374,21 +376,29 @@ auto async_connection_wal_checkpoint(noto::connection connection, std::string mo
} }
noto::sqlite_value::sqlite_value(std::nullptr_t) : value{std::monostate{}} {} noto::sqlite_value::sqlite_value(std::nullptr_t)
: value{std::monostate{}} {}
noto::sqlite_value::sqlite_value(std::int32_t value) : value{static_cast<std::int64_t>(value)} {} noto::sqlite_value::sqlite_value(std::int32_t value)
: value{static_cast<std::int64_t>(value)} {}
noto::sqlite_value::sqlite_value(std::int64_t current) : value{current} {} noto::sqlite_value::sqlite_value(std::int64_t current)
: value{current} {}
noto::sqlite_value::sqlite_value(std::float64_t current) : value{current} {} noto::sqlite_value::sqlite_value(std::float64_t current)
: value{current} {}
noto::sqlite_value::sqlite_value(std::char_t const* current) : value{std::string{current == nullptr ? "" : current}} {} noto::sqlite_value::sqlite_value(std::char_t const *current)
: value{std::string{current == nullptr ? "" : current}} {}
noto::sqlite_value::sqlite_value(std::string current) : value{std::move(current)} {} noto::sqlite_value::sqlite_value(std::string current)
: value{std::move(current)} {}
noto::sqlite_value::sqlite_value(std::string_view current) : value{std::string{current}} {} noto::sqlite_value::sqlite_value(std::string_view current)
: value{std::string{current}} {}
noto::sqlite_value::sqlite_value(std::vector<std::byte> current) : value{std::move(current)} {} noto::sqlite_value::sqlite_value(std::vector<std::byte> current)
: value{std::move(current)} {}
auto noto::sqlite_value::is_null() const -> std::bool_t { auto noto::sqlite_value::is_null() const -> std::bool_t {
return std::holds_alternative<std::monostate>(value); return std::holds_alternative<std::monostate>(value);
@@ -424,9 +434,11 @@ auto noto::statement::async_finalize() -> boost::asio::awaitable<std::expected<v
return async_statement_finalize(std::move(core)); return async_statement_finalize(std::move(core));
} }
noto::statement::statement(std::shared_ptr<noto::detail::statement_core> core) : core_{std::move(core)} {} noto::statement::statement(std::shared_ptr<noto::detail::statement_core> core)
: core_{std::move(core)} {}
noto::connection::connection(boost::asio::any_io_executor executor, noto::connection_config config) : core_{std::make_shared<noto::detail::connection_core>(std::move(executor), std::move(config))} {} noto::connection::connection(boost::asio::any_io_executor executor, noto::connection_config config)
: core_{std::make_shared<noto::detail::connection_core>(std::move(executor), std::move(config))} {}
noto::connection::~connection() = default; noto::connection::~connection() = default;
@@ -586,7 +598,8 @@ auto noto::connection::async_wal_checkpoint(std::string mode) -> boost::asio::aw
return async_connection_wal_checkpoint(noto::connection{core_}, std::move(mode)); return async_connection_wal_checkpoint(noto::connection{core_}, std::move(mode));
} }
noto::connection::connection(std::shared_ptr<noto::detail::connection_core> core) : core_{std::move(core)} {} noto::connection::connection(std::shared_ptr<noto::detail::connection_core> core)
: core_{std::move(core)} {}
auto noto::connection::core() const -> std::shared_ptr<noto::detail::connection_core> { auto noto::connection::core() const -> std::shared_ptr<noto::detail::connection_core> {
return core_; return core_;
+136 -135
View File
@@ -56,23 +56,24 @@
#include <noto/detail/core.hpp> #include <noto/detail/core.hpp>
namespace { namespace {
struct pending_statement_finalize { struct pending_statement_finalize {
pending_statement_finalize(std::shared_ptr<noto::detail::connection_core> owner_value, sqlite3_stmt* statement_value, std::bool_t counted_value); pending_statement_finalize(std::shared_ptr<noto::detail::connection_core> owner_value, sqlite3_stmt *statement_value, std::bool_t counted_value);
pending_statement_finalize(pending_statement_finalize const&) = delete; pending_statement_finalize(pending_statement_finalize const &) = delete;
auto operator=(pending_statement_finalize const&) -> pending_statement_finalize& = delete; auto operator=(pending_statement_finalize const &) -> pending_statement_finalize & = delete;
~pending_statement_finalize(); ~pending_statement_finalize();
auto finish() noexcept -> void; auto finish() noexcept -> void;
std::shared_ptr<noto::detail::connection_core> owner{}; std::shared_ptr<noto::detail::connection_core> owner{};
sqlite3_stmt* statement{}; sqlite3_stmt *statement{};
std::bool_t counted{}; std::bool_t counted{};
}; };
struct pending_transaction_rollback { struct pending_transaction_rollback {
explicit pending_transaction_rollback(std::shared_ptr<noto::detail::connection_core> owner_value); explicit pending_transaction_rollback(std::shared_ptr<noto::detail::connection_core> owner_value);
pending_transaction_rollback(pending_transaction_rollback const&) = delete; pending_transaction_rollback(pending_transaction_rollback const &) = delete;
auto operator=(pending_transaction_rollback const&) -> pending_transaction_rollback& = delete; auto operator=(pending_transaction_rollback const &) -> pending_transaction_rollback & = delete;
~pending_transaction_rollback(); ~pending_transaction_rollback();
auto finish() noexcept -> void; auto finish() noexcept -> void;
std::shared_ptr<noto::detail::connection_core> owner{}; std::shared_ptr<noto::detail::connection_core> owner{};
@@ -169,10 +170,11 @@ auto errno_status(std::string operation, std::int32_t code) -> std::expected<voi
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + std::error_code{ return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + std::error_code{
code, code,
std::generic_category(), std::generic_category(),
}.message()); }
.message());
} }
auto filesystem_status(std::string operation, std::error_code const& code) -> std::expected<void, modulus::error> { auto filesystem_status(std::string operation, std::error_code const &code) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + code.message()); return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + code.message());
} }
@@ -183,7 +185,7 @@ auto close_fd(std::int32_t fd, std::string_view operation) -> std::expected<void
return errno_status(std::string{operation}, errno); return errno_status(std::string{operation}, errno);
} }
auto sync_file_path(std::string const& path, std::string_view operation) -> std::expected<void, modulus::error> { auto sync_file_path(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
auto fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); auto fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) { if (fd < 0) {
return errno_status(std::string{operation}, errno); return errno_status(std::string{operation}, errno);
@@ -196,7 +198,7 @@ auto sync_file_path(std::string const& path, std::string_view operation) -> std:
return close_fd(fd, operation); return close_fd(fd, operation);
} }
auto sync_parent_directory(std::string const& path, std::string_view operation) -> std::expected<void, modulus::error> { auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
auto parent = std::filesystem::path{path}.parent_path(); auto parent = std::filesystem::path{path}.parent_path();
if (parent.empty()) { if (parent.empty()) {
parent = "."; parent = ".";
@@ -214,7 +216,7 @@ auto sync_parent_directory(std::string const& path, std::string_view operation)
return close_fd(fd, operation); return close_fd(fd, operation);
} }
auto make_temporary_path(std::string const& target, std::string_view operation) -> std::expected<std::string, modulus::error> { auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected<std::string, modulus::error> {
auto target_path = std::filesystem::path{target}; auto target_path = std::filesystem::path{target};
auto parent = target_path.parent_path(); auto parent = target_path.parent_path();
if (parent.empty()) { if (parent.empty()) {
@@ -243,20 +245,20 @@ auto make_temporary_path(std::string const& target, std::string_view operation)
return path; return path;
} }
auto remove_file_noexcept(std::string const& path) noexcept -> void { auto remove_file_noexcept(std::string const &path) noexcept -> void {
if (!path.empty()) { if (!path.empty()) {
static_cast<void>(::unlink(path.c_str())); static_cast<void>(::unlink(path.c_str()));
} }
} }
auto remove_file_if_exists(std::string const& path, std::string_view operation) -> std::expected<void, modulus::error> { auto remove_file_if_exists(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
if (::unlink(path.c_str()) == 0 || errno == ENOENT) { if (::unlink(path.c_str()) == 0 || errno == ENOENT) {
return {}; return {};
} }
return errno_status(std::string{operation}, errno); return errno_status(std::string{operation}, errno);
} }
auto remove_database_sidecars(std::string const& path, std::string_view operation) -> std::expected<void, modulus::error> { auto remove_database_sidecars(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
for (auto suffix : sqlite_sidecar_suffixes) { for (auto suffix : sqlite_sidecar_suffixes) {
auto status = remove_file_if_exists(path + std::string{suffix}, operation); auto status = remove_file_if_exists(path + std::string{suffix}, operation);
if (!status) { if (!status) {
@@ -266,14 +268,14 @@ auto remove_database_sidecars(std::string const& path, std::string_view operatio
return {}; return {};
} }
auto remove_database_files_noexcept(std::string const& path) noexcept -> void { auto remove_database_files_noexcept(std::string const &path) noexcept -> void {
remove_file_noexcept(path); remove_file_noexcept(path);
for (auto suffix : sqlite_sidecar_suffixes) { for (auto suffix : sqlite_sidecar_suffixes) {
remove_file_noexcept(path + std::string{suffix}); remove_file_noexcept(path + std::string{suffix});
} }
} }
auto replace_file_atomically(std::string const& temporary, std::string const& destination, std::string_view operation) -> std::expected<void, modulus::error> { auto replace_file_atomically(std::string const &temporary, std::string const &destination, std::string_view operation) -> std::expected<void, modulus::error> {
auto status = sync_file_path(temporary, operation); auto status = sync_file_path(temporary, operation);
if (!status) { if (!status) {
return status; return status;
@@ -294,7 +296,7 @@ auto validate_database_name(std::string_view name, std::string_view label) -> st
return {}; return {};
} }
auto open_flags(noto::connection_config const& config) -> std::int32_t { auto open_flags(noto::connection_config const &config) -> std::int32_t {
auto flags = SQLITE_OPEN_FULLMUTEX; auto flags = SQLITE_OPEN_FULLMUTEX;
switch (config.mode) { switch (config.mode) {
case noto::open_mode::READ_ONLY: case noto::open_mode::READ_ONLY:
@@ -340,7 +342,7 @@ auto transaction_mode_sql(noto::transaction_mode mode) -> std::string_view {
return "BEGIN IMMEDIATE"; return "BEGIN IMMEDIATE";
} }
auto begin_operation(noto::detail::connection_core& core, std::string_view operation) -> std::expected<void, modulus::error> { auto begin_operation(noto::detail::connection_core &core, std::string_view operation) -> std::expected<void, modulus::error> {
if (core.current_state() != noto::connection_state::CONNECTED || core.database == nullptr) { if (core.current_state() != noto::connection_state::CONNECTED || core.database == nullptr) {
return unavailable(std::string{operation} + " requires an open database connection"); return unavailable(std::string{operation} + " requires an open database connection");
} }
@@ -351,7 +353,7 @@ auto begin_operation(noto::detail::connection_core& core, std::string_view opera
return {}; return {};
} }
auto finish_operation(noto::detail::connection_core& core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> { auto finish_operation(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
core.busy = false; core.busy = false;
if (!status && status.error().code() == noto::detail::error_data_loss) { if (!status && status.error().code() == noto::detail::error_data_loss) {
core.set_state(noto::connection_state::FAILED); core.set_state(noto::connection_state::FAILED);
@@ -359,7 +361,7 @@ auto finish_operation(noto::detail::connection_core& core, std::expected<void, m
return status; return status;
} }
auto validate_backup_options(noto::backup_options const& options) -> std::expected<void, modulus::error> { auto validate_backup_options(noto::backup_options const &options) -> std::expected<void, modulus::error> {
if (options.pages_per_step <= 0) { if (options.pages_per_step <= 0) {
return invalid("backup pages per step must be positive"); return invalid("backup pages per step must be positive");
} }
@@ -384,15 +386,15 @@ auto validate_backup_options(noto::backup_options const& options) -> std::expect
return {}; return {};
} }
auto validate_export_options(noto::export_options const& options) -> std::expected<void, modulus::error> { auto validate_export_options(noto::export_options const &options) -> std::expected<void, modulus::error> {
if (options.rows_per_yield == 0) { if (options.rows_per_yield == 0) {
return invalid("SQL export rows per yield must be positive"); return invalid("SQL export rows per yield must be positive");
} }
return {}; return {};
} }
auto exec_direct(sqlite3* database, std::string const& sql, std::string_view operation) -> std::expected<void, modulus::error> { auto exec_direct(sqlite3 *database, std::string const &sql, std::string_view operation) -> std::expected<void, modulus::error> {
auto* error = static_cast<std::char_t*>(nullptr); auto *error = static_cast<std::char_t *>(nullptr);
auto code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, &error); auto code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, &error);
if (code == SQLITE_OK) { if (code == SQLITE_OK) {
return {}; return {};
@@ -408,12 +410,12 @@ auto exec_direct(sqlite3* database, std::string const& sql, std::string_view ope
return noto::detail::fail_void(mapped.code(), std::move(message)); return noto::detail::fail_void(mapped.code(), std::move(message));
} }
auto configure_busy_timeout(sqlite3* database, std::chrono::milliseconds timeout, std::string_view operation) -> std::expected<void, modulus::error> { auto configure_busy_timeout(sqlite3 *database, std::chrono::milliseconds timeout, std::string_view operation) -> std::expected<void, modulus::error> {
return noto::result_from(sqlite3_busy_timeout(database, static_cast<std::int32_t>(timeout.count())), database, operation); return noto::result_from(sqlite3_busy_timeout(database, static_cast<std::int32_t>(timeout.count())), database, operation);
} }
auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const& value) -> std::expected<std::string, modulus::error> { auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const &value) -> std::expected<std::string, modulus::error> {
return std::visit([](auto const& current) -> std::expected<std::string, modulus::error> { return std::visit([](auto const &current) -> std::expected<std::string, modulus::error> {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::bool_t>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::bool_t>) {
return std::string{current ? "1" : "0"}; return std::string{current ? "1" : "0"};
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) { } else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
@@ -421,10 +423,11 @@ auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const
} else { } else {
return noto::detail::quote_sql_literal(current, "pragma value"); return noto::detail::quote_sql_literal(current, "pragma value");
} }
}, value); },
value);
} }
auto set_pragma(sqlite3* database, std::string const& name, std::variant<std::bool_t, std::int64_t, std::string> const& value) -> std::expected<void, modulus::error> { auto set_pragma(sqlite3 *database, std::string const &name, std::variant<std::bool_t, std::int64_t, std::string> const &value) -> std::expected<void, modulus::error> {
if (!valid_token(name)) { if (!valid_token(name)) {
return invalid("database pragma names must contain only letters, digits, or underscores"); return invalid("database pragma names must contain only letters, digits, or underscores");
} }
@@ -435,9 +438,9 @@ auto set_pragma(sqlite3* database, std::string const& name, std::variant<std::bo
return exec_direct(database, "PRAGMA " + name + " = " + *rendered, "set pragma"); return exec_direct(database, "PRAGMA " + name + " = " + *rendered, "set pragma");
} }
auto open_configured_database(noto::connection_config const& config, std::chrono::milliseconds busy_timeout, sqlite3** output) -> std::expected<void, modulus::error> { auto open_configured_database(noto::connection_config const &config, std::chrono::milliseconds busy_timeout, sqlite3 **output) -> std::expected<void, modulus::error> {
*output = nullptr; *output = nullptr;
auto* next_database = static_cast<sqlite3*>(nullptr); auto *next_database = static_cast<sqlite3 *>(nullptr);
auto filename = config.mode == noto::open_mode::MEMORY ? std::string{":memory:"} : config.filename; auto filename = config.mode == noto::open_mode::MEMORY ? std::string{":memory:"} : config.filename;
auto code = sqlite3_open_v2(filename.c_str(), &next_database, open_flags(config), nullptr); auto code = sqlite3_open_v2(filename.c_str(), &next_database, open_flags(config), nullptr);
if (code != SQLITE_OK) { if (code != SQLITE_OK) {
@@ -452,7 +455,7 @@ auto open_configured_database(noto::connection_config const& config, std::chrono
if (status) { if (status) {
status = exec_direct(next_database, std::string{"PRAGMA foreign_keys = "} + (config.foreign_keys ? "1" : "0"), "configure foreign keys"); status = exec_direct(next_database, std::string{"PRAGMA foreign_keys = "} + (config.foreign_keys ? "1" : "0"), "configure foreign keys");
} }
for (auto const& pragma : config.pragmas) { for (auto const &pragma : config.pragmas) {
if (!status) { if (!status) {
break; break;
} }
@@ -466,11 +469,11 @@ auto open_configured_database(noto::connection_config const& config, std::chrono
return {}; return {};
} }
auto retry_timeout(noto::detail::connection_core const& core, noto::backup_options const& options) -> std::chrono::milliseconds { auto retry_timeout(noto::detail::connection_core const &core, noto::backup_options const &options) -> std::chrono::milliseconds {
return options.retry_timeout.value_or(core.busy_timeout); return options.retry_timeout.value_or(core.busy_timeout);
} }
auto async_step_backup(noto::detail::connection_core& core, sqlite3_backup* backup, noto::backup_options const& options, std::string_view operation) -> boost::asio::awaitable<std::expected<std::int32_t, modulus::error>> { auto async_step_backup(noto::detail::connection_core &core, sqlite3_backup *backup, noto::backup_options const &options, std::string_view operation) -> boost::asio::awaitable<std::expected<std::int32_t, modulus::error>> {
auto const deadline = std::chrono::steady_clock::now() + retry_timeout(core, options); auto const deadline = std::chrono::steady_clock::now() + retry_timeout(core, options);
auto timer = boost::asio::steady_timer{core.strand}; auto timer = boost::asio::steady_timer{core.strand};
while (true) { while (true) {
@@ -482,7 +485,6 @@ auto async_step_backup(noto::detail::connection_core& core, sqlite3_backup* back
co_await boost::asio::post(core.strand, boost::asio::use_awaitable); co_await boost::asio::post(core.strand, boost::asio::use_awaitable);
continue; continue;
} }
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
if (now >= deadline) { if (now >= deadline) {
co_return std::unexpected(timeout(std::string{operation} + " timed out while waiting for SQLite locks").error()); co_return std::unexpected(timeout(std::string{operation} + " timed out while waiting for SQLite locks").error());
@@ -500,7 +502,7 @@ auto async_step_backup(noto::detail::connection_core& core, sqlite3_backup* back
} }
} }
auto close_sqlite_handle(sqlite3* handle, std::string_view operation, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> { auto close_sqlite_handle(sqlite3 *handle, std::string_view operation, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
auto close_code = sqlite3_close(handle); auto close_code = sqlite3_close(handle);
if (close_code == SQLITE_OK) { if (close_code == SQLITE_OK) {
return status; return status;
@@ -512,8 +514,8 @@ auto close_sqlite_handle(sqlite3* handle, std::string_view operation, std::expec
return status; return status;
} }
auto async_copy_restore_source_to_file(noto::detail::connection_core& core, std::string const& path, std::string const& temporary, noto::backup_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto async_copy_restore_source_to_file(noto::detail::connection_core &core, std::string const &path, std::string const &temporary, noto::backup_options const &options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto* source = static_cast<sqlite3*>(nullptr); auto *source = static_cast<sqlite3 *>(nullptr);
auto code = sqlite3_open_v2(path.c_str(), &source, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr); auto code = sqlite3_open_v2(path.c_str(), &source, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr);
if (code != SQLITE_OK) { if (code != SQLITE_OK) {
auto status = noto::result_from(code, source, "open restore source"); auto status = noto::result_from(code, source, "open restore source");
@@ -527,8 +529,7 @@ auto async_copy_restore_source_to_file(noto::detail::connection_core& core, std:
sqlite3_close(source); sqlite3_close(source);
co_return status; co_return status;
} }
auto *destination = static_cast<sqlite3 *>(nullptr);
auto* destination = static_cast<sqlite3*>(nullptr);
code = sqlite3_open_v2(temporary.c_str(), &destination, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr); code = sqlite3_open_v2(temporary.c_str(), &destination, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr);
if (code != SQLITE_OK) { if (code != SQLITE_OK) {
status = noto::result_from(code, destination, "open restore temporary destination"); status = noto::result_from(code, destination, "open restore temporary destination");
@@ -544,8 +545,7 @@ auto async_copy_restore_source_to_file(noto::detail::connection_core& core, std:
sqlite3_close(source); sqlite3_close(source);
co_return status; co_return status;
} }
auto *backup = sqlite3_backup_init(destination, "main", source, options.source_name.c_str());
auto* backup = sqlite3_backup_init(destination, "main", source, options.source_name.c_str());
if (backup == nullptr) { if (backup == nullptr) {
status = noto::result_from(sqlite3_errcode(destination), destination, "initialize restore"); status = noto::result_from(sqlite3_errcode(destination), destination, "initialize restore");
status = close_sqlite_handle(destination, "close restore temporary destination", status); status = close_sqlite_handle(destination, "close restore temporary destination", status);
@@ -560,12 +560,12 @@ auto async_copy_restore_source_to_file(noto::detail::connection_core& core, std:
co_return status; co_return status;
} }
auto close_database_for_atomic_restore(noto::detail::connection_core& core) -> std::expected<void, modulus::error> { auto close_database_for_atomic_restore(noto::detail::connection_core &core) -> std::expected<void, modulus::error> {
auto checkpoint = noto::result_from(sqlite3_wal_checkpoint_v2(core.database, nullptr, SQLITE_CHECKPOINT_TRUNCATE, nullptr, nullptr), core.database, "checkpoint database before restore"); auto checkpoint = noto::result_from(sqlite3_wal_checkpoint_v2(core.database, nullptr, SQLITE_CHECKPOINT_TRUNCATE, nullptr, nullptr), core.database, "checkpoint database before restore");
if (!checkpoint) { if (!checkpoint) {
return checkpoint; return checkpoint;
} }
auto* closing = std::exchange(core.database, nullptr); auto *closing = std::exchange(core.database, nullptr);
auto code = sqlite3_close(closing); auto code = sqlite3_close(closing);
if (code == SQLITE_OK) { if (code == SQLITE_OK) {
return {}; return {};
@@ -574,8 +574,8 @@ auto close_database_for_atomic_restore(noto::detail::connection_core& core) -> s
return noto::result_from(code, core.database, "close database before restore"); return noto::result_from(code, core.database, "close database before restore");
} }
auto reopen_database_after_restore(noto::detail::connection_core& core) -> std::expected<void, modulus::error> { auto reopen_database_after_restore(noto::detail::connection_core &core) -> std::expected<void, modulus::error> {
auto* reopened = static_cast<sqlite3*>(nullptr); auto *reopened = static_cast<sqlite3 *>(nullptr);
auto status = open_configured_database(core.config, core.busy_timeout, &reopened); auto status = open_configured_database(core.config, core.busy_timeout, &reopened);
if (!status) { if (!status) {
core.set_state(noto::connection_state::FAILED); core.set_state(noto::connection_state::FAILED);
@@ -586,7 +586,7 @@ auto reopen_database_after_restore(noto::detail::connection_core& core) -> std::
return {}; return {};
} }
auto restore_atomic_failure(noto::detail::connection_core& core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> { auto restore_atomic_failure(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
if (core.database != nullptr) { if (core.database != nullptr) {
core.set_state(noto::connection_state::CONNECTED); core.set_state(noto::connection_state::CONNECTED);
return status; return status;
@@ -595,11 +595,11 @@ auto restore_atomic_failure(noto::detail::connection_core& core, std::expected<v
return reopened ? status : reopened; return reopened ? status : reopened;
} }
auto statement_is_open(noto::detail::statement_core const& core) -> std::bool_t { auto statement_is_open(noto::detail::statement_core const &core) -> std::bool_t {
return core.statement != nullptr && !core.finalized.load(std::memory_order_acquire); return core.statement != nullptr && !core.finalized.load(std::memory_order_acquire);
} }
auto untrack_statement(noto::detail::statement_core& statement) -> void { auto untrack_statement(noto::detail::statement_core &statement) -> void {
if (!statement.counted) { if (!statement.counted) {
return; return;
} }
@@ -609,19 +609,19 @@ auto untrack_statement(noto::detail::statement_core& statement) -> void {
} }
} }
auto bind_value(sqlite3_stmt *statement, std::int32_t index, noto::sqlite_value const &value, sqlite3 *database) -> std::expected<void, modulus::error> {
auto bind_value(sqlite3_stmt* statement, std::int32_t index, noto::sqlite_value const& value, sqlite3* database) -> std::expected<void, modulus::error> { auto size = std::visit([](auto const &current) -> std::size_t {
auto size = std::visit([](auto const& current) -> std::size_t {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::string> || std::is_same_v<std::decay_t<decltype(current)>, std::vector<std::byte>>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::string> || std::is_same_v<std::decay_t<decltype(current)>, std::vector<std::byte>>) {
return current.size(); return current.size();
} else { } else {
return std::size_t{}; return std::size_t{};
} }
}, value.value); },
value.value);
if (size > sqlite_int_max()) { if (size > sqlite_int_max()) {
return invalid("bound SQLite text/blob value is too large"); return invalid("bound SQLite text/blob value is too large");
} }
auto code = std::visit([statement, index](auto const& current) -> std::int32_t { auto code = std::visit([statement, index](auto const &current) -> std::int32_t {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
return sqlite3_bind_null(statement, index); return sqlite3_bind_null(statement, index);
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) { } else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
@@ -633,11 +633,12 @@ auto bind_value(sqlite3_stmt* statement, std::int32_t index, noto::sqlite_value
} else { } else {
return sqlite3_bind_blob(statement, index, current.data(), static_cast<std::int32_t>(current.size()), SQLITE_TRANSIENT); return sqlite3_bind_blob(statement, index, current.data(), static_cast<std::int32_t>(current.size()), SQLITE_TRANSIENT);
} }
}, value.value); },
value.value);
return noto::result_from(code, database, "bind parameter"); return noto::result_from(code, database, "bind parameter");
} }
auto bind_values(sqlite3_stmt* statement, std::vector<noto::sqlite_value> const& params, sqlite3* database) -> std::expected<void, modulus::error> { auto bind_values(sqlite3_stmt *statement, std::vector<noto::sqlite_value> const &params, sqlite3 *database) -> std::expected<void, modulus::error> {
if (params.size() > sqlite_int_max()) { if (params.size() > sqlite_int_max()) {
return invalid("prepared statement parameter count is too large"); return invalid("prepared statement parameter count is too large");
} }
@@ -654,14 +655,14 @@ auto bind_values(sqlite3_stmt* statement, std::vector<noto::sqlite_value> const&
return {}; return {};
} }
auto column_value(sqlite3_stmt* statement, std::int32_t index) -> noto::sqlite_value { auto column_value(sqlite3_stmt *statement, std::int32_t index) -> noto::sqlite_value {
switch (sqlite3_column_type(statement, index)) { switch (sqlite3_column_type(statement, index)) {
case SQLITE_INTEGER: case SQLITE_INTEGER:
return noto::sqlite_value{static_cast<std::int64_t>(sqlite3_column_int64(statement, index))}; return noto::sqlite_value{static_cast<std::int64_t>(sqlite3_column_int64(statement, index))};
case SQLITE_FLOAT: case SQLITE_FLOAT:
return noto::sqlite_value{sqlite3_column_double(statement, index)}; return noto::sqlite_value{sqlite3_column_double(statement, index)};
case SQLITE_TEXT: { case SQLITE_TEXT: {
auto const* text = reinterpret_cast<std::char_t const*>(sqlite3_column_text(statement, index)); auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement, index));
auto bytes = sqlite3_column_bytes(statement, index); auto bytes = sqlite3_column_bytes(statement, index);
return noto::sqlite_value{ return noto::sqlite_value{
text == nullptr ? std::string{} : std::string{ text == nullptr ? std::string{} : std::string{
@@ -671,7 +672,7 @@ auto column_value(sqlite3_stmt* statement, std::int32_t index) -> noto::sqlite_v
}; };
} }
case SQLITE_BLOB: { case SQLITE_BLOB: {
auto const* data = static_cast<std::byte const*>(sqlite3_column_blob(statement, index)); auto const *data = static_cast<std::byte const *>(sqlite3_column_blob(statement, index));
auto bytes = sqlite3_column_bytes(statement, index); auto bytes = sqlite3_column_bytes(statement, index);
auto blob = std::vector<std::byte>{}; auto blob = std::vector<std::byte>{};
if (data != nullptr && bytes > 0) { if (data != nullptr && bytes > 0) {
@@ -685,7 +686,7 @@ auto column_value(sqlite3_stmt* statement, std::int32_t index) -> noto::sqlite_v
} }
} }
auto current_row(sqlite3_stmt* statement) -> std::vector<noto::sqlite_value> { auto current_row(sqlite3_stmt *statement) -> std::vector<noto::sqlite_value> {
auto count = sqlite3_column_count(statement); auto count = sqlite3_column_count(statement);
auto row = std::vector<noto::sqlite_value>{}; auto row = std::vector<noto::sqlite_value>{};
row.reserve(static_cast<std::size_t>(count)); row.reserve(static_cast<std::size_t>(count));
@@ -695,19 +696,19 @@ auto current_row(sqlite3_stmt* statement) -> std::vector<noto::sqlite_value> {
return row; return row;
} }
auto column_names(sqlite3_stmt* statement) -> std::vector<std::string> { auto column_names(sqlite3_stmt *statement) -> std::vector<std::string> {
auto count = sqlite3_column_count(statement); auto count = sqlite3_column_count(statement);
auto names = std::vector<std::string>{}; auto names = std::vector<std::string>{};
names.reserve(static_cast<std::size_t>(count)); names.reserve(static_cast<std::size_t>(count));
for (auto index = 0; index < count; ++index) { for (auto index = 0; index < count; ++index) {
auto const* name = sqlite3_column_name(statement, index); auto const *name = sqlite3_column_name(statement, index);
names.push_back(name == nullptr ? std::string{} : std::string{name}); names.push_back(name == nullptr ? std::string{} : std::string{name});
} }
return names; return names;
} }
auto value_result_bytes(noto::sqlite_value const& value) -> std::size_t { auto value_result_bytes(noto::sqlite_value const &value) -> std::size_t {
return std::visit([](auto const& current) -> std::size_t { return std::visit([](auto const &current) -> std::size_t {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
return {}; return {};
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) { } else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
@@ -717,10 +718,11 @@ auto value_result_bytes(noto::sqlite_value const& value) -> std::size_t {
} else { } else {
return current.size(); return current.size();
} }
}, value.value); },
value.value);
} }
auto add_result_bytes(std::size_t& current, std::size_t addition, noto::execute_options const& options) -> std::expected<void, modulus::error> { auto add_result_bytes(std::size_t &current, std::size_t addition, noto::execute_options const &options) -> std::expected<void, modulus::error> {
if (!options.max_result_bytes) { if (!options.max_result_bytes) {
if (addition > std::numeric_limits<std::size_t>::max() - current) { if (addition > std::numeric_limits<std::size_t>::max() - current) {
current = std::numeric_limits<std::size_t>::max(); current = std::numeric_limits<std::size_t>::max();
@@ -736,8 +738,8 @@ auto add_result_bytes(std::size_t& current, std::size_t addition, noto::execute_
return {}; return {};
} }
auto track_result_columns(std::vector<std::string> const& columns, std::size_t& result_bytes, noto::execute_options const& options) -> std::expected<void, modulus::error> { auto track_result_columns(std::vector<std::string> const &columns, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected<void, modulus::error> {
for (auto const& column : columns) { for (auto const &column : columns) {
auto status = add_result_bytes(result_bytes, column.size(), options); auto status = add_result_bytes(result_bytes, column.size(), options);
if (!status) { if (!status) {
return status; return status;
@@ -746,11 +748,11 @@ auto track_result_columns(std::vector<std::string> const& columns, std::size_t&
return {}; return {};
} }
auto append_result_row(noto::query_result& result, std::vector<noto::sqlite_value> row, std::size_t& result_bytes, noto::execute_options const& options) -> std::expected<void, modulus::error> { auto append_result_row(noto::query_result &result, std::vector<noto::sqlite_value> row, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected<void, modulus::error> {
if (options.max_rows && result.rows.size() >= *options.max_rows) { if (options.max_rows && result.rows.size() >= *options.max_rows) {
return resource_exhausted("query returned more rows than configured limit"); return resource_exhausted("query returned more rows than configured limit");
} }
for (auto const& value : row) { for (auto const &value : row) {
auto status = add_result_bytes(result_bytes, value_result_bytes(value), options); auto status = add_result_bytes(result_bytes, value_result_bytes(value), options);
if (!status) { if (!status) {
return status; return status;
@@ -760,12 +762,12 @@ auto append_result_row(noto::query_result& result, std::vector<noto::sqlite_valu
return {}; return {};
} }
auto reset_statement_after_execute_failure(sqlite3_stmt* statement, sqlite3* database, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> { auto reset_statement_after_execute_failure(sqlite3_stmt *statement, sqlite3 *database, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
auto reset = noto::result_from(sqlite3_reset(statement), database, "reset statement after execute failure"); auto reset = noto::result_from(sqlite3_reset(statement), database, "reset statement after execute failure");
return reset ? status : reset; return reset ? status : reset;
} }
auto trailing_sql_is_empty(std::char_t const* tail) -> std::bool_t { auto trailing_sql_is_empty(std::char_t const *tail) -> std::bool_t {
if (tail == nullptr) { if (tail == nullptr) {
return true; return true;
} }
@@ -778,8 +780,8 @@ auto trailing_sql_is_empty(std::char_t const* tail) -> std::bool_t {
return true; return true;
} }
auto prepare_statement(sqlite3* database, std::string const& sql, sqlite3_stmt** statement, std::string_view operation) -> std::expected<void, modulus::error> { auto prepare_statement(sqlite3 *database, std::string const &sql, sqlite3_stmt **statement, std::string_view operation) -> std::expected<void, modulus::error> {
auto const* tail = static_cast<std::char_t const*>(nullptr); auto const *tail = static_cast<std::char_t const *>(nullptr);
auto code = sqlite3_prepare_v2(database, sql.c_str(), -1, statement, &tail); auto code = sqlite3_prepare_v2(database, sql.c_str(), -1, statement, &tail);
auto status = noto::result_from(code, database, operation); auto status = noto::result_from(code, database, operation);
if (!status) { if (!status) {
@@ -796,11 +798,11 @@ auto prepare_statement(sqlite3* database, std::string const& sql, sqlite3_stmt**
return {}; return {};
} }
auto same_identity(file_identity const& left, file_identity const& right) -> std::bool_t { auto same_identity(file_identity const &left, file_identity const &right) -> std::bool_t {
return left.device == right.device && left.inode == right.inode; return left.device == right.device && left.inode == right.inode;
} }
auto normalized_path(std::string const& path, std::string_view operation) -> std::expected<std::filesystem::path, modulus::error> { auto normalized_path(std::string const &path, std::string_view operation) -> std::expected<std::filesystem::path, modulus::error> {
auto error = std::error_code{}; auto error = std::error_code{};
auto absolute = std::filesystem::absolute(path, error); auto absolute = std::filesystem::absolute(path, error);
if (error) { if (error) {
@@ -813,8 +815,9 @@ auto normalized_path(std::string const& path, std::string_view operation) -> std
return normalized.lexically_normal(); return normalized.lexically_normal();
} }
auto identity_for_path(std::filesystem::path const& path, std::string_view operation) -> std::expected<std::optional<file_identity>, modulus::error> { auto identity_for_path(std::filesystem::path const &path, std::string_view operation) -> std::expected<std::optional<file_identity>, modulus::error> {
struct stat data {}; struct stat data{};
auto text = path.string(); auto text = path.string();
if (::stat(text.c_str(), &data) == 0) { if (::stat(text.c_str(), &data) == 0) {
auto identity = file_identity{}; auto identity = file_identity{};
@@ -828,7 +831,7 @@ auto identity_for_path(std::filesystem::path const& path, std::string_view opera
return std::unexpected(errno_status(std::string{operation}, errno).error()); return std::unexpected(errno_status(std::string{operation}, errno).error());
} }
auto protected_path_for(std::string const& path, std::string_view operation) -> std::expected<protected_path, modulus::error> { auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected<protected_path, modulus::error> {
auto normalized = normalized_path(path, operation); auto normalized = normalized_path(path, operation);
if (!normalized) { if (!normalized) {
return std::unexpected(normalized.error()); return std::unexpected(normalized.error());
@@ -843,15 +846,15 @@ auto protected_path_for(std::string const& path, std::string_view operation) ->
}; };
} }
auto same_protected_path(protected_path const& left, protected_path const& right) -> std::bool_t { auto same_protected_path(protected_path const &left, protected_path const &right) -> std::bool_t {
if (left.normalized == right.normalized) { if (left.normalized == right.normalized) {
return true; return true;
} }
return left.identity && right.identity && same_identity(*left.identity, *right.identity); return left.identity && right.identity && same_identity(*left.identity, *right.identity);
} }
auto live_database_paths(sqlite3* database) -> std::expected<std::vector<std::string>, modulus::error> { auto live_database_paths(sqlite3 *database) -> std::expected<std::vector<std::string>, modulus::error> {
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list"); auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list");
if (!prepared) { if (!prepared) {
return failure_from<std::vector<std::string>>(prepared); return failure_from<std::vector<std::string>>(prepared);
@@ -872,7 +875,7 @@ auto live_database_paths(sqlite3* database) -> std::expected<std::vector<std::st
if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) { if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) {
continue; continue;
} }
auto const* text = reinterpret_cast<std::char_t const*>(sqlite3_column_text(statement.get(), 2)); auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement.get(), 2));
auto bytes = sqlite3_column_bytes(statement.get(), 2); auto bytes = sqlite3_column_bytes(statement.get(), 2);
auto path = text == nullptr ? std::string{} : std::string{ auto path = text == nullptr ? std::string{} : std::string{
text, text,
@@ -884,7 +887,7 @@ auto live_database_paths(sqlite3* database) -> std::expected<std::vector<std::st
} }
} }
auto validate_output_not_live_database(sqlite3* database, std::string const& path, std::string_view label) -> std::expected<void, modulus::error> { auto validate_output_not_live_database(sqlite3 *database, std::string const &path, std::string_view label) -> std::expected<void, modulus::error> {
auto target = protected_path_for(path, std::string{"check "} + std::string{label}); auto target = protected_path_for(path, std::string{"check "} + std::string{label});
if (!target) { if (!target) {
return std::unexpected(target.error()); return std::unexpected(target.error());
@@ -893,7 +896,7 @@ auto validate_output_not_live_database(sqlite3* database, std::string const& pat
if (!live_paths) { if (!live_paths) {
return std::unexpected(live_paths.error()); return std::unexpected(live_paths.error());
} }
for (auto const& live_path : *live_paths) { for (auto const &live_path : *live_paths) {
auto protected_live = protected_path_for(live_path, "check open database path"); auto protected_live = protected_path_for(live_path, "check open database path");
if (!protected_live) { if (!protected_live) {
return std::unexpected(protected_live.error()); return std::unexpected(protected_live.error());
@@ -914,8 +917,8 @@ auto validate_output_not_live_database(sqlite3* database, std::string const& pat
return {}; return {};
} }
auto execute_prepared(sqlite3* database, std::string const& sql, std::vector<noto::sqlite_value> const& params, noto::execute_options const& options) -> std::expected<noto::query_result, modulus::error> { auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector<noto::sqlite_value> const &params, noto::execute_options const &options) -> std::expected<noto::query_result, modulus::error> {
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement"); auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement");
if (!prepared) { if (!prepared) {
return failure_from<noto::query_result>(prepared); return failure_from<noto::query_result>(prepared);
@@ -959,7 +962,7 @@ auto execute_prepared(sqlite3* database, std::string const& sql, std::vector<not
} }
} }
auto hex_literal(std::byte const* data, std::size_t size) -> std::string { auto hex_literal(std::byte const *data, std::size_t size) -> std::string {
auto sql = std::ostringstream{}; auto sql = std::ostringstream{};
sql << "X'"; sql << "X'";
for (auto index = std::size_t{}; index < size; ++index) { for (auto index = std::size_t{}; index < size; ++index) {
@@ -969,13 +972,13 @@ auto hex_literal(std::byte const* data, std::size_t size) -> std::string {
return sql.str(); return sql.str();
} }
auto text_hex_literal(std::string const& value) -> std::string { auto text_hex_literal(std::string const &value) -> std::string {
auto const* bytes = reinterpret_cast<std::byte const*>(value.data()); auto const *bytes = reinterpret_cast<std::byte const *>(value.data());
return "CAST(" + hex_literal(bytes, value.size()) + " AS TEXT)"; return "CAST(" + hex_literal(bytes, value.size()) + " AS TEXT)";
} }
auto literal_sql(noto::sqlite_value const& value) -> std::expected<std::string, modulus::error> { auto literal_sql(noto::sqlite_value const &value) -> std::expected<std::string, modulus::error> {
return std::visit([](auto const& current) -> std::expected<std::string, modulus::error> { return std::visit([](auto const &current) -> std::expected<std::string, modulus::error> {
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) { if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
return "NULL"; return "NULL";
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) { } else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
@@ -999,10 +1002,11 @@ auto literal_sql(noto::sqlite_value const& value) -> std::expected<std::string,
} else { } else {
return hex_literal(current.data(), current.size()); return hex_literal(current.data(), current.size());
} }
}, value.value); },
value.value);
} }
auto write_limited(std::ostream& output, std::string_view text, noto::export_options const& options, std::size_t& written) -> std::expected<void, modulus::error> { auto write_limited(std::ostream &output, std::string_view text, noto::export_options const &options, std::size_t &written) -> std::expected<void, modulus::error> {
if (text.size() > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) { if (text.size() > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
return resource_exhausted("SQL export chunk is too large"); return resource_exhausted("SQL export chunk is too large");
} }
@@ -1021,11 +1025,11 @@ auto write_limited(std::ostream& output, std::string_view text, noto::export_opt
return {}; return {};
} }
auto column_text(sqlite3_stmt* statement, std::int32_t index) -> std::optional<std::string> { auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional<std::string> {
if (sqlite3_column_type(statement, index) != SQLITE_TEXT) { if (sqlite3_column_type(statement, index) != SQLITE_TEXT) {
return std::nullopt; return std::nullopt;
} }
auto const* text = reinterpret_cast<std::char_t const*>(sqlite3_column_text(statement, index)); auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement, index));
auto bytes = sqlite3_column_bytes(statement, index); auto bytes = sqlite3_column_bytes(statement, index);
return text == nullptr ? std::string{} : std::string{ return text == nullptr ? std::string{} : std::string{
text, text,
@@ -1038,7 +1042,7 @@ auto identifier_character(std::char_t character) -> std::bool_t {
return std::isalnum(byte) || character == '_'; return std::isalnum(byte) || character == '_';
} }
auto consume_keyword(std::string_view sql, std::size_t& position, std::string_view keyword) -> std::bool_t { auto consume_keyword(std::string_view sql, std::size_t &position, std::string_view keyword) -> std::bool_t {
auto current = position; auto current = position;
while (current < sql.size() && std::isspace(static_cast<std::uchar_t>(sql[current]))) { while (current < sql.size() && std::isspace(static_cast<std::uchar_t>(sql[current]))) {
++current; ++current;
@@ -1073,8 +1077,8 @@ auto create_virtual_table_sql(std::string_view sql) -> std::bool_t {
return consume_keyword(sql, position, "VIRTUAL") && consume_keyword(sql, position, "TABLE"); return consume_keyword(sql, position, "VIRTUAL") && consume_keyword(sql, position, "TABLE");
} }
auto table_exists(sqlite3* database, std::string const& table_name) -> std::expected<std::bool_t, modulus::error> { auto table_exists(sqlite3 *database, std::string const &table_name) -> std::expected<std::bool_t, modulus::error> {
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, "SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1", &raw_statement, "prepare table existence check"); auto prepared = prepare_statement(database, "SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1", &raw_statement, "prepare table existence check");
if (!prepared) { if (!prepared) {
return failure_from<std::bool_t>(prepared); return failure_from<std::bool_t>(prepared);
@@ -1098,12 +1102,12 @@ auto table_exists(sqlite3* database, std::string const& table_name) -> std::expe
return failure_from<std::bool_t>(noto::result_from(code, database, "check table existence")); return failure_from<std::bool_t>(noto::result_from(code, database, "check table existence"));
} }
auto exportable_columns(sqlite3* database, std::string const& table_name) -> std::expected<std::vector<export_column>, modulus::error> { auto exportable_columns(sqlite3 *database, std::string const &table_name) -> std::expected<std::vector<export_column>, modulus::error> {
auto quoted_table = noto::quote_identifier(table_name); auto quoted_table = noto::quote_identifier(table_name);
if (!quoted_table) { if (!quoted_table) {
return failure_from<std::vector<export_column>>(quoted_table.error()); return failure_from<std::vector<export_column>>(quoted_table.error());
} }
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, "PRAGMA table_xinfo(" + *quoted_table + ")", &raw_statement, "prepare export columns"); auto prepared = prepare_statement(database, "PRAGMA table_xinfo(" + *quoted_table + ")", &raw_statement, "prepare export columns");
if (!prepared) { if (!prepared) {
return failure_from<std::vector<export_column>>(prepared); return failure_from<std::vector<export_column>>(prepared);
@@ -1142,15 +1146,11 @@ auto exportable_columns(sqlite3* database, std::string const& table_name) -> std
} }
} }
auto collect_export_schema(sqlite3* database) -> std::expected<export_schema, modulus::error> { auto collect_export_schema(sqlite3 *database) -> std::expected<export_schema, modulus::error> {
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement( auto prepared = prepare_statement(database, "SELECT type, name, sql FROM sqlite_schema WHERE sql IS NOT NULL AND name NOT GLOB 'sqlite_*' "
database,
"SELECT type, name, sql FROM sqlite_schema WHERE sql IS NOT NULL AND name NOT GLOB 'sqlite_*' "
"ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 WHEN 'trigger' THEN 2 WHEN 'view' THEN 3 ELSE 4 END, name", "ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 WHEN 'trigger' THEN 2 WHEN 'view' THEN 3 ELSE 4 END, name",
&raw_statement, &raw_statement, "prepare export schema");
"prepare export schema"
);
if (!prepared) { if (!prepared) {
return failure_from<export_schema>(prepared); return failure_from<export_schema>(prepared);
} }
@@ -1202,8 +1202,8 @@ auto collect_export_schema(sqlite3* database) -> std::expected<export_schema, mo
} }
} }
auto export_table_rows(noto::detail::connection_core& core, export_table const& table, std::function<std::expected<void, modulus::error>(std::string_view)> const& write, noto::export_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto export_table_rows(noto::detail::connection_core &core, export_table const &table, std::function<std::expected<void, modulus::error>(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto select_sql = std::string{}; auto select_sql = std::string{};
if (table.columns.empty()) { if (table.columns.empty()) {
select_sql = "SELECT 1 FROM " + table.quoted_name; select_sql = "SELECT 1 FROM " + table.quoted_name;
@@ -1271,12 +1271,12 @@ auto export_table_rows(noto::detail::connection_core& core, export_table const&
} }
} }
auto export_sqlite_sequence(noto::detail::connection_core& core, std::function<std::expected<void, modulus::error>(std::string_view)> const& write, noto::export_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto export_sqlite_sequence(noto::detail::connection_core &core, std::function<std::expected<void, modulus::error>(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto status = write("DELETE FROM \"sqlite_sequence\";\n"); auto status = write("DELETE FROM \"sqlite_sequence\";\n");
if (!status) { if (!status) {
co_return status; co_return status;
} }
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(core.database, "SELECT name, seq FROM sqlite_sequence", &raw_statement, "prepare sqlite_sequence export"); auto prepared = prepare_statement(core.database, "SELECT name, seq FROM sqlite_sequence", &raw_statement, "prepare sqlite_sequence export");
if (!prepared) { if (!prepared) {
co_return prepared; co_return prepared;
@@ -1316,7 +1316,7 @@ auto export_sqlite_sequence(noto::detail::connection_core& core, std::function<s
} }
} }
auto write_export(noto::detail::connection_core& core, std::function<std::expected<void, modulus::error>(std::string_view)> const& write, noto::export_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto write_export(noto::detail::connection_core &core, std::function<std::expected<void, modulus::error>(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto schema = collect_export_schema(core.database); auto schema = collect_export_schema(core.database);
if (!schema) { if (!schema) {
co_return std::unexpected(schema.error()); co_return std::unexpected(schema.error());
@@ -1325,7 +1325,7 @@ auto write_export(noto::detail::connection_core& core, std::function<std::expect
if (!status) { if (!status) {
co_return status; co_return status;
} }
for (auto const& sql : schema->table_sql) { for (auto const &sql : schema->table_sql) {
status = write(sql); status = write(sql);
if (status) { if (status) {
status = write(";\n"); status = write(";\n");
@@ -1334,7 +1334,7 @@ auto write_export(noto::detail::connection_core& core, std::function<std::expect
co_return status; co_return status;
} }
} }
for (auto const& table : schema->tables) { for (auto const &table : schema->tables) {
status = co_await export_table_rows(core, table, write, options); status = co_await export_table_rows(core, table, write, options);
if (!status) { if (!status) {
co_return status; co_return status;
@@ -1346,7 +1346,7 @@ auto write_export(noto::detail::connection_core& core, std::function<std::expect
co_return status; co_return status;
} }
} }
for (auto const& sql : schema->post_data_sql) { for (auto const &sql : schema->post_data_sql) {
status = write(sql); status = write(sql);
if (status) { if (status) {
status = write(";\n"); status = write(";\n");
@@ -1358,14 +1358,15 @@ auto write_export(noto::detail::connection_core& core, std::function<std::expect
co_return write("COMMIT;\nPRAGMA foreign_keys=ON;\n"); co_return write("COMMIT;\nPRAGMA foreign_keys=ON;\n");
} }
pending_statement_finalize::pending_statement_finalize(std::shared_ptr<noto::detail::connection_core> owner_value, sqlite3_stmt* statement_value, std::bool_t counted_value) : owner{std::move(owner_value)}, statement{statement_value}, counted{counted_value} {} pending_statement_finalize::pending_statement_finalize(std::shared_ptr<noto::detail::connection_core> owner_value, sqlite3_stmt *statement_value, std::bool_t counted_value)
: owner{std::move(owner_value)}, statement{statement_value}, counted{counted_value} {}
pending_statement_finalize::~pending_statement_finalize() { pending_statement_finalize::~pending_statement_finalize() {
finish(); finish();
} }
auto pending_statement_finalize::finish() noexcept -> void { auto pending_statement_finalize::finish() noexcept -> void {
auto* current = std::exchange(statement, nullptr); auto *current = std::exchange(statement, nullptr);
if (current != nullptr) { if (current != nullptr) {
static_cast<void>(sqlite3_finalize(current)); static_cast<void>(sqlite3_finalize(current));
} }
@@ -1375,7 +1376,8 @@ auto pending_statement_finalize::finish() noexcept -> void {
counted = false; counted = false;
} }
pending_transaction_rollback::pending_transaction_rollback(std::shared_ptr<noto::detail::connection_core> owner_value) : owner{std::move(owner_value)} {} pending_transaction_rollback::pending_transaction_rollback(std::shared_ptr<noto::detail::connection_core> owner_value)
: owner{std::move(owner_value)} {}
pending_transaction_rollback::~pending_transaction_rollback() { pending_transaction_rollback::~pending_transaction_rollback() {
finish(); finish();
@@ -1394,7 +1396,8 @@ auto pending_transaction_rollback::finish() noexcept -> void {
} }
noto::detail::connection_core::connection_core(boost::asio::any_io_executor executor_value, noto::connection_config config_value) : executor{std::move(executor_value)}, strand{boost::asio::make_strand(executor)}, config{std::move(config_value)}, busy_timeout{config.busy_timeout} {} noto::detail::connection_core::connection_core(boost::asio::any_io_executor executor_value, noto::connection_config config_value)
: executor{std::move(executor_value)}, strand{boost::asio::make_strand(executor)}, config{std::move(config_value)}, busy_timeout{config.busy_timeout} {}
noto::detail::connection_core::~connection_core() { noto::detail::connection_core::~connection_core() {
if (database != nullptr) { if (database != nullptr) {
@@ -1447,7 +1450,7 @@ auto noto::detail::connection_core::async_connect() -> boost::asio::awaitable<st
co_return valid; co_return valid;
} }
busy = true; busy = true;
auto* next_database = static_cast<sqlite3*>(nullptr); auto *next_database = static_cast<sqlite3 *>(nullptr);
auto status = open_configured_database(config, busy_timeout, &next_database); auto status = open_configured_database(config, busy_timeout, &next_database);
if (!status) { if (!status) {
busy = false; busy = false;
@@ -1482,7 +1485,7 @@ auto noto::detail::connection_core::async_close() -> boost::asio::awaitable<std:
} }
transaction_active = false; transaction_active = false;
} }
auto* closing = std::exchange(database, nullptr); auto *closing = std::exchange(database, nullptr);
auto code = sqlite3_close(closing); auto code = sqlite3_close(closing);
if (code != SQLITE_OK) { if (code != SQLITE_OK) {
database = closing; database = closing;
@@ -1520,7 +1523,7 @@ auto noto::detail::connection_core::async_prepare(std::string sql) -> boost::asi
if (!ready) { if (!ready) {
co_return std::unexpected(ready.error()); co_return std::unexpected(ready.error());
} }
auto* raw_statement = static_cast<sqlite3_stmt*>(nullptr); auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement"); auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement");
auto status = finish_operation(*this, prepared); auto status = finish_operation(*this, prepared);
if (!status) { if (!status) {
@@ -1631,7 +1634,7 @@ auto noto::detail::connection_core::async_backup_to_file(std::string path, noto:
if (!temporary) { if (!temporary) {
co_return finish_operation(*this, noto::detail::fail_void(temporary.error())); co_return finish_operation(*this, noto::detail::fail_void(temporary.error()));
} }
auto* destination = static_cast<sqlite3*>(nullptr); auto *destination = static_cast<sqlite3 *>(nullptr);
auto code = sqlite3_open_v2(temporary->c_str(), &destination, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr); auto code = sqlite3_open_v2(temporary->c_str(), &destination, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr);
if (code != SQLITE_OK) { if (code != SQLITE_OK) {
auto status = noto::result_from(code, destination, "open backup destination"); auto status = noto::result_from(code, destination, "open backup destination");
@@ -1651,7 +1654,7 @@ auto noto::detail::connection_core::async_backup_to_file(std::string path, noto:
auto restored = configure_busy_timeout(database, busy_timeout, "restore source busy timeout after backup"); auto restored = configure_busy_timeout(database, busy_timeout, "restore source busy timeout after backup");
return status ? restored : status; return status ? restored : status;
}; };
auto* backup = sqlite3_backup_init(destination, options.destination_name.c_str(), database, options.source_name.c_str()); auto *backup = sqlite3_backup_init(destination, options.destination_name.c_str(), database, options.source_name.c_str());
if (backup == nullptr) { if (backup == nullptr) {
auto status = noto::result_from(sqlite3_errcode(destination), destination, "initialize backup"); auto status = noto::result_from(sqlite3_errcode(destination), destination, "initialize backup");
sqlite3_close(destination); sqlite3_close(destination);
@@ -1759,9 +1762,7 @@ auto noto::detail::connection_core::async_export_sql(noto::export_options option
} }
auto output = std::ostringstream{}; auto output = std::ostringstream{};
auto written = std::size_t{}; auto written = std::size_t{};
auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { return write_limited(output, text, options, written); }, options);
return write_limited(output, text, options, written);
}, options);
if (!status) { if (!status) {
status = finish_operation(*this, status); status = finish_operation(*this, status);
co_return std::unexpected(status.error()); co_return std::unexpected(status.error());
@@ -1802,9 +1803,7 @@ auto noto::detail::connection_core::async_export_sql_to_file(std::string path, n
co_return finish_operation(*this, unavailable("failed to open export file")); co_return finish_operation(*this, unavailable("failed to open export file"));
} }
auto written = std::size_t{}; auto written = std::size_t{};
auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { return write_limited(output, text, options, written); }, options);
return write_limited(output, text, options, written);
}, options);
if (status) { if (status) {
output.flush(); output.flush();
if (!output) { if (!output) {
@@ -1842,7 +1841,8 @@ auto noto::detail::connection_core::async_set_busy_timeout(std::chrono::millisec
co_return finish_operation(*this, status); co_return finish_operation(*this, status);
} }
noto::detail::statement_core::statement_core(std::shared_ptr<noto::detail::connection_core> connection_value, sqlite3_stmt* statement_value) : connection{std::move(connection_value)}, statement{statement_value}, counted{statement_value != nullptr} { noto::detail::statement_core::statement_core(std::shared_ptr<noto::detail::connection_core> connection_value, sqlite3_stmt *statement_value)
: connection{std::move(connection_value)}, statement{statement_value}, counted{statement_value != nullptr} {
if (connection && counted) { if (connection && counted) {
connection->live_statements.fetch_add(1, std::memory_order_acq_rel); connection->live_statements.fetch_add(1, std::memory_order_acq_rel);
} }
@@ -1852,7 +1852,7 @@ noto::detail::statement_core::~statement_core() {
if (finalized.exchange(true, std::memory_order_acq_rel)) { if (finalized.exchange(true, std::memory_order_acq_rel)) {
return; return;
} }
auto* current = std::exchange(statement, nullptr); auto *current = std::exchange(statement, nullptr);
auto owner = connection; auto owner = connection;
auto was_counted = counted; auto was_counted = counted;
counted = false; counted = false;
@@ -2005,14 +2005,15 @@ auto noto::detail::statement_core::async_finalize() -> boost::asio::awaitable<st
if (!ready) { if (!ready) {
co_return ready; co_return ready;
} }
auto* current = std::exchange(statement, nullptr); auto *current = std::exchange(statement, nullptr);
finalized.store(true, std::memory_order_release); finalized.store(true, std::memory_order_release);
auto status = noto::result_from(sqlite3_finalize(current), owner->database, "finalize statement"); auto status = noto::result_from(sqlite3_finalize(current), owner->database, "finalize statement");
untrack_statement(*this); untrack_statement(*this);
co_return finish_operation(*owner, status); co_return finish_operation(*owner, status);
} }
noto::detail::transaction_state::transaction_state(std::shared_ptr<noto::detail::connection_core> core_value, std::function<void()> on_finish_value) : core{std::move(core_value)}, on_finish{std::move(on_finish_value)}, active{true} {} noto::detail::transaction_state::transaction_state(std::shared_ptr<noto::detail::connection_core> core_value, std::function<void()> on_finish_value)
: core{std::move(core_value)}, on_finish{std::move(on_finish_value)}, active{true} {}
auto noto::detail::transaction_state::finish() noexcept -> void { auto noto::detail::transaction_state::finish() noexcept -> void {
if (!on_finish) { if (!on_finish) {
+2 -1
View File
@@ -8,6 +8,7 @@
#include <modulus.hpp> #include <modulus.hpp>
auto noto::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error { auto noto::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
return modulus::make_error("noto", code, std::move(message), location); return modulus::make_error("noto", code, std::move(message), location);
} }
@@ -20,7 +21,7 @@ auto noto::detail::fail_void(std::int32_t code, std::string message, modulus::so
return noto::detail::fail_void(noto::detail::make_error(code, std::move(message), location)); return noto::detail::fail_void(noto::detail::make_error(code, std::move(message), location));
} }
auto noto::detail::fail_void(std::expected<void, modulus::error> const& result) -> std::expected<void, modulus::error> { auto noto::detail::fail_void(std::expected<void, modulus::error> const &result) -> std::expected<void, modulus::error> {
assert(!result); assert(!result);
return noto::detail::fail_void(result.error()); return noto::detail::fail_void(result.error());
} }
+1
View File
@@ -9,6 +9,7 @@
#include <noto/detail/errors.hpp> #include <noto/detail/errors.hpp>
auto noto::detail::quote_sql_literal(std::string_view value, std::string_view label, std::bool_t allow_empty) -> std::expected<std::string, modulus::error> { auto noto::detail::quote_sql_literal(std::string_view value, std::string_view label, std::bool_t allow_empty) -> std::expected<std::string, modulus::error> {
if (value.empty() && !allow_empty) { if (value.empty() && !allow_empty) {
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, std::string{label} + " must not be empty"); return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, std::string{label} + " must not be empty");
+4 -3
View File
@@ -11,6 +11,7 @@
#include <noto/detail/errors.hpp> #include <noto/detail/errors.hpp>
namespace { namespace {
auto code_from(std::int32_t code) -> std::int32_t { auto code_from(std::int32_t code) -> std::int32_t {
@@ -51,7 +52,7 @@ auto code_from(std::int32_t code) -> std::int32_t {
} }
} }
auto message_from(std::int32_t code, sqlite3* database, std::string_view operation) -> std::string { auto message_from(std::int32_t code, sqlite3 *database, std::string_view operation) -> std::string {
auto message = std::string{}; auto message = std::string{};
if (!operation.empty()) { if (!operation.empty()) {
message += operation; message += operation;
@@ -67,11 +68,11 @@ auto message_from(std::int32_t code, sqlite3* database, std::string_view operati
} }
auto noto::error_from(std::int32_t code, sqlite3* database, std::string_view operation) -> modulus::error { auto noto::error_from(std::int32_t code, sqlite3 *database, std::string_view operation) -> modulus::error {
return noto::detail::make_error(code_from(code), message_from(code, database, operation)); return noto::detail::make_error(code_from(code), message_from(code, database, operation));
} }
auto noto::result_from(std::int32_t code, sqlite3* database, std::string_view operation) -> std::expected<void, modulus::error> { auto noto::result_from(std::int32_t code, sqlite3 *database, std::string_view operation) -> std::expected<void, modulus::error> {
if (code == SQLITE_OK || code == SQLITE_ROW || code == SQLITE_DONE) { if (code == SQLITE_OK || code == SQLITE_ROW || code == SQLITE_DONE) {
return {}; return {};
} }
+26 -21
View File
@@ -23,6 +23,7 @@
#include <noto/transaction.hpp> #include <noto/transaction.hpp>
#include <cstdint> #include <cstdint>
namespace { namespace {
auto invalid(std::string message) -> std::expected<void, modulus::error> { auto invalid(std::string message) -> std::expected<void, modulus::error> {
@@ -37,7 +38,7 @@ auto contains_null(std::string_view value) -> std::bool_t {
return value.find('\0') != std::string_view::npos; return value.find('\0') != std::string_view::npos;
} }
auto qualified_table(noto::table_name const& table) -> std::expected<std::string, modulus::error> { auto qualified_table(noto::table_name const &table) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(table.name); auto name = noto::quote_identifier(table.name);
if (!name) { if (!name) {
return std::unexpected(name.error()); return std::unexpected(name.error());
@@ -52,15 +53,18 @@ auto qualified_table(noto::table_name const& table) -> std::expected<std::string
return *database + "." + *name; return *database + "." + *name;
} }
auto migration_table_sql(noto::migration_options const& options) -> std::expected<std::string, modulus::error> { auto migration_table_sql(noto::migration_options const &options) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table); auto table = qualified_table(options.table);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
} }
return "CREATE TABLE IF NOT EXISTS " + *table + " (\"id\" TEXT NOT NULL PRIMARY KEY, " "\"description\" TEXT NOT NULL, " "\"checksum\" TEXT NOT NULL, " "\"applied_at\" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) STRICT"; return "CREATE TABLE IF NOT EXISTS " + *table + " (\"id\" TEXT NOT NULL PRIMARY KEY, "
"\"description\" TEXT NOT NULL, "
"\"checksum\" TEXT NOT NULL, "
"\"applied_at\" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) STRICT";
} }
auto select_records_sql(noto::migration_options const& options) -> std::expected<std::string, modulus::error> { auto select_records_sql(noto::migration_options const &options) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table); auto table = qualified_table(options.table);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
@@ -68,7 +72,7 @@ auto select_records_sql(noto::migration_options const& options) -> std::expected
return "SELECT \"id\", \"description\", \"checksum\", \"applied_at\" FROM " + *table + " ORDER BY \"id\""; return "SELECT \"id\", \"description\", \"checksum\", \"applied_at\" FROM " + *table + " ORDER BY \"id\"";
} }
auto insert_record_sql(noto::migration_options const& options, noto::migration const& migration) -> std::expected<std::string, modulus::error> { auto insert_record_sql(noto::migration_options const &options, noto::migration const &migration) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table); auto table = qualified_table(options.table);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
@@ -88,7 +92,7 @@ auto insert_record_sql(noto::migration_options const& options, noto::migration c
return "INSERT INTO " + *table + " (\"id\", \"description\", \"checksum\") VALUES (" + *id + ", " + *description + ", " + *checksum + ")"; return "INSERT INTO " + *table + " (\"id\", \"description\", \"checksum\") VALUES (" + *id + ", " + *description + ", " + *checksum + ")";
} }
auto delete_record_sql(noto::migration_options const& options, std::string_view id_value) -> std::expected<std::string, modulus::error> { auto delete_record_sql(noto::migration_options const &options, std::string_view id_value) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table); auto table = qualified_table(options.table);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
@@ -102,7 +106,7 @@ auto delete_record_sql(noto::migration_options const& options, std::string_view
auto validate_and_sort(std::vector<noto::migration> migrations, std::bool_t require_up) -> std::expected<std::vector<noto::migration>, modulus::error> { auto validate_and_sort(std::vector<noto::migration> migrations, std::bool_t require_up) -> std::expected<std::vector<noto::migration>, modulus::error> {
auto ids = std::set<std::string>{}; auto ids = std::set<std::string>{};
for (auto const& migration : migrations) { for (auto const &migration : migrations) {
if (migration.id.empty()) { if (migration.id.empty()) {
return std::unexpected(invalid("migration id must not be empty").error()); return std::unexpected(invalid("migration id must not be empty").error());
} }
@@ -126,7 +130,7 @@ auto validate_and_sort(std::vector<noto::migration> migrations, std::bool_t requ
return migrations; return migrations;
} }
auto as_string(noto::sqlite_value const& value) -> std::string { auto as_string(noto::sqlite_value const &value) -> std::string {
if (auto text = std::get_if<std::string>(&value.value)) { if (auto text = std::get_if<std::string>(&value.value)) {
return *text; return *text;
} }
@@ -136,7 +140,7 @@ auto as_string(noto::sqlite_value const& value) -> std::string {
return {}; return {};
} }
auto execute_status(noto::connection& connection, std::string sql) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto execute_status(noto::connection &connection, std::string sql) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto result = co_await connection.async_execute(std::move(sql)); auto result = co_await connection.async_execute(std::move(sql));
if (!result) { if (!result) {
co_return std::unexpected(result.error()); co_return std::unexpected(result.error());
@@ -144,7 +148,7 @@ auto execute_status(noto::connection& connection, std::string sql) -> boost::asi
co_return std::expected<void, modulus::error>{}; co_return std::expected<void, modulus::error>{};
} }
auto ensure_migration_table(noto::connection& connection, noto::migration_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto ensure_migration_table(noto::connection &connection, noto::migration_options const &options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = migration_table_sql(options); auto sql = migration_table_sql(options);
if (!sql) { if (!sql) {
co_return std::unexpected(sql.error()); co_return std::unexpected(sql.error());
@@ -152,7 +156,7 @@ auto ensure_migration_table(noto::connection& connection, noto::migration_option
co_return co_await execute_status(connection, *sql); co_return co_await execute_status(connection, *sql);
} }
auto load_records(noto::connection& connection, noto::migration_options const& options) -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> { auto load_records(noto::connection &connection, noto::migration_options const &options) -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> {
auto sql = select_records_sql(options); auto sql = select_records_sql(options);
if (!sql) { if (!sql) {
co_return std::unexpected(sql.error()); co_return std::unexpected(sql.error());
@@ -163,7 +167,7 @@ auto load_records(noto::connection& connection, noto::migration_options const& o
} }
auto records = std::vector<noto::migration_record>{}; auto records = std::vector<noto::migration_record>{};
records.reserve(result->rows.size()); records.reserve(result->rows.size());
for (auto const& row : result->rows) { for (auto const &row : result->rows) {
if (row.size() < 4) { if (row.size() < 4) {
co_return std::unexpected(internal("migration table returned an incomplete row").error()); co_return std::unexpected(internal("migration table returned an incomplete row").error());
} }
@@ -177,8 +181,8 @@ auto load_records(noto::connection& connection, noto::migration_options const& o
co_return records; co_return records;
} }
auto applied_record(std::vector<noto::migration_record> const& records, std::string const& id) -> noto::migration_record const* { auto applied_record(std::vector<noto::migration_record> const &records, std::string const &id) -> noto::migration_record const * {
for (auto const& record : records) { for (auto const &record : records) {
if (record.id == id) { if (record.id == id) {
return &record; return &record;
} }
@@ -186,7 +190,7 @@ auto applied_record(std::vector<noto::migration_record> const& records, std::str
return nullptr; return nullptr;
} }
auto rollback_and_return(noto::transaction& transaction, std::expected<void, modulus::error> status) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto rollback_and_return(noto::transaction &transaction, std::expected<void, modulus::error> status) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto rollback = co_await transaction.async_rollback(); auto rollback = co_await transaction.async_rollback();
if (!rollback) { if (!rollback) {
co_return rollback; co_return rollback;
@@ -194,13 +198,14 @@ auto rollback_and_return(noto::transaction& transaction, std::expected<void, mod
co_return status; co_return status;
} }
auto rollback_and_return(noto::transaction& transaction, modulus::error error) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto rollback_and_return(noto::transaction &transaction, modulus::error error) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await rollback_and_return(transaction, noto::detail::fail_void(std::move(error))); co_return co_await rollback_and_return(transaction, noto::detail::fail_void(std::move(error)));
} }
} }
noto::migrator::migrator(noto::connection& connection, noto::migration_options options) : connection_{&connection}, options_{std::move(options)} {} noto::migrator::migrator(noto::connection &connection, noto::migration_options options)
: connection_{&connection}, options_{std::move(options)} {}
auto noto::migrator::async_applied_migrations() -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> { auto noto::migrator::async_applied_migrations() -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> {
if (connection_ == nullptr) { if (connection_ == nullptr) {
@@ -235,8 +240,8 @@ auto noto::migrator::async_migrate(std::vector<noto::migration> migrations) -> b
if (!records) { if (!records) {
co_return co_await rollback_and_return(transaction, records.error()); co_return co_await rollback_and_return(transaction, records.error());
} }
for (auto const& migration : *sorted) { for (auto const &migration : *sorted) {
auto const* applied = applied_record(*records, migration.id); auto const *applied = applied_record(*records, migration.id);
if (applied != nullptr) { if (applied != nullptr) {
if (applied->checksum != migration.checksum) { if (applied->checksum != migration.checksum) {
co_return co_await rollback_and_return(transaction, invalid("applied migration checksum mismatch: " + migration.id)); co_return co_await rollback_and_return(transaction, invalid("applied migration checksum mismatch: " + migration.id));
@@ -277,7 +282,7 @@ auto noto::migrator::async_rollback(std::vector<noto::migration> migrations, std
co_return std::unexpected(sorted.error()); co_return std::unexpected(sorted.error());
} }
auto by_id = std::map<std::string, noto::migration>{}; auto by_id = std::map<std::string, noto::migration>{};
for (auto& migration : *sorted) { for (auto &migration : *sorted) {
by_id.emplace(migration.id, std::move(migration)); by_id.emplace(migration.id, std::move(migration));
} }
auto ensured = co_await ensure_migration_table(*connection_, options_); auto ensured = co_await ensure_migration_table(*connection_, options_);
@@ -294,7 +299,7 @@ auto noto::migrator::async_rollback(std::vector<noto::migration> migrations, std
co_return co_await rollback_and_return(transaction, records.error()); co_return co_await rollback_and_return(transaction, records.error());
} }
std::ranges::sort(*records, std::greater<>{}, &noto::migration_record::id); std::ranges::sort(*records, std::greater<>{}, &noto::migration_record::id);
for (auto const& record : *records) { for (auto const &record : *records) {
if (!target_id.empty() && record.id <= target_id) { if (!target_id.empty() && record.id <= target_id) {
continue; continue;
} }
+28 -24
View File
@@ -15,6 +15,7 @@
#include <noto/detail/errors.hpp> #include <noto/detail/errors.hpp>
#include <noto/detail/sql.hpp> #include <noto/detail/sql.hpp>
namespace { namespace {
auto invalid_sql(std::string message) -> std::expected<std::string, modulus::error> { auto invalid_sql(std::string message) -> std::expected<std::string, modulus::error> {
@@ -50,7 +51,7 @@ auto validate_fragment(std::string_view value, std::string_view label) -> std::e
return {}; return {};
} }
auto qualified_table(noto::table_name const& table) -> std::expected<std::string, modulus::error> { auto qualified_table(noto::table_name const &table) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(table.name); auto name = noto::quote_identifier(table.name);
if (!name) { if (!name) {
return std::unexpected(name.error()); return std::unexpected(name.error());
@@ -91,7 +92,7 @@ auto reference_action_sql(noto::reference_action action) -> std::string_view {
return "RESTRICT"; return "RESTRICT";
} }
auto append_identifier_list(std::ostringstream& sql, std::vector<std::string> const& identifiers) -> std::expected<void, modulus::error> { auto append_identifier_list(std::ostringstream &sql, std::vector<std::string> const &identifiers) -> std::expected<void, modulus::error> {
if (identifiers.empty()) { if (identifiers.empty()) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "column list must not be empty"); return noto::detail::fail_void(noto::detail::error_invalid_argument, "column list must not be empty");
} }
@@ -108,7 +109,7 @@ auto append_identifier_list(std::ostringstream& sql, std::vector<std::string> co
return {}; return {};
} }
auto append_constraint_name(std::ostringstream& sql, std::string const& name) -> std::expected<void, modulus::error> { auto append_constraint_name(std::ostringstream &sql, std::string const &name) -> std::expected<void, modulus::error> {
if (name.empty()) { if (name.empty()) {
return {}; return {};
} }
@@ -120,7 +121,7 @@ auto append_constraint_name(std::ostringstream& sql, std::string const& name) ->
return {}; return {};
} }
auto column_sql(noto::column_definition const& column) -> std::expected<std::string, modulus::error> { auto column_sql(noto::column_definition const &column) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(column.name); auto name = noto::quote_identifier(column.name);
if (!name) { if (!name) {
return std::unexpected(name.error()); return std::unexpected(name.error());
@@ -167,7 +168,7 @@ auto column_sql(noto::column_definition const& column) -> std::expected<std::str
return sql.str(); return sql.str();
} }
auto primary_key_sql(std::vector<std::string> const& columns) -> std::expected<std::string, modulus::error> { auto primary_key_sql(std::vector<std::string> const &columns) -> std::expected<std::string, modulus::error> {
auto sql = std::ostringstream{}; auto sql = std::ostringstream{};
sql << "PRIMARY KEY ("; sql << "PRIMARY KEY (";
auto status = append_identifier_list(sql, columns); auto status = append_identifier_list(sql, columns);
@@ -178,7 +179,7 @@ auto primary_key_sql(std::vector<std::string> const& columns) -> std::expected<s
return sql.str(); return sql.str();
} }
auto unique_constraint_sql(noto::unique_constraint_definition const& definition) -> std::expected<std::string, modulus::error> { auto unique_constraint_sql(noto::unique_constraint_definition const &definition) -> std::expected<std::string, modulus::error> {
auto sql = std::ostringstream{}; auto sql = std::ostringstream{};
auto name_status = append_constraint_name(sql, definition.name); auto name_status = append_constraint_name(sql, definition.name);
if (!name_status) { if (!name_status) {
@@ -193,7 +194,7 @@ auto unique_constraint_sql(noto::unique_constraint_definition const& definition)
return sql.str(); return sql.str();
} }
auto check_constraint_sql(noto::check_constraint_definition const& definition) -> std::expected<std::string, modulus::error> { auto check_constraint_sql(noto::check_constraint_definition const &definition) -> std::expected<std::string, modulus::error> {
if (!definition.enforced) { if (!definition.enforced) {
return invalid_sql("SQLite check constraints cannot be marked not enforced"); return invalid_sql("SQLite check constraints cannot be marked not enforced");
} }
@@ -210,7 +211,7 @@ auto check_constraint_sql(noto::check_constraint_definition const& definition) -
return sql.str(); return sql.str();
} }
auto foreign_key_sql(noto::foreign_key_definition const& definition) -> std::expected<std::string, modulus::error> { auto foreign_key_sql(noto::foreign_key_definition const &definition) -> std::expected<std::string, modulus::error> {
auto target = qualified_table(definition.referenced_table); auto target = qualified_table(definition.referenced_table);
if (!target) { if (!target) {
return std::unexpected(target.error()); return std::unexpected(target.error());
@@ -240,7 +241,7 @@ auto foreign_key_sql(noto::foreign_key_definition const& definition) -> std::exp
return sql.str(); return sql.str();
} }
auto append_table_options(std::ostringstream& sql, noto::table_options const& options) -> void { auto append_table_options(std::ostringstream &sql, noto::table_options const &options) -> void {
auto first = true; auto first = true;
auto append = [&sql, &first](std::string_view value) { auto append = [&sql, &first](std::string_view value) {
sql << (first ? " " : ", ") << value; sql << (first ? " " : ", ") << value;
@@ -264,7 +265,7 @@ auto noto::trusted_sql_fragment::unsafe(std::string sql) -> std::expected<noto::
return noto::trusted_sql_fragment{std::move(sql)}; return noto::trusted_sql_fragment{std::move(sql)};
} }
auto noto::trusted_sql_fragment::value() const -> std::string const& { auto noto::trusted_sql_fragment::value() const -> std::string const & {
return sql_; return sql_;
} }
@@ -272,7 +273,8 @@ auto noto::trusted_sql_fragment::empty() const -> std::bool_t {
return sql_.empty(); return sql_.empty();
} }
noto::trusted_sql_fragment::trusted_sql_fragment(std::string sql) : sql_{std::move(sql)} {} noto::trusted_sql_fragment::trusted_sql_fragment(std::string sql)
: sql_{std::move(sql)} {}
auto noto::column_type::integer() -> noto::column_type { auto noto::column_type::integer() -> noto::column_type {
return noto::column_type{"INTEGER"}; return noto::column_type{"INTEGER"};
@@ -323,7 +325,7 @@ auto noto::column_type::trusted(noto::trusted_sql_fragment fragment) -> noto::co
return noto::column_type{fragment.value()}; return noto::column_type{fragment.value()};
} }
auto noto::column_type::sql() const -> std::string const& { auto noto::column_type::sql() const -> std::string const & {
return sql_; return sql_;
} }
@@ -331,7 +333,8 @@ auto noto::column_type::empty() const -> std::bool_t {
return sql_.empty(); return sql_.empty();
} }
noto::column_type::column_type(std::string sql) : sql_{std::move(sql)} {} noto::column_type::column_type(std::string sql)
: sql_{std::move(sql)} {}
auto noto::default_value::null_value() -> noto::default_value { auto noto::default_value::null_value() -> noto::default_value {
return noto::default_value{"NULL"}; return noto::default_value{"NULL"};
@@ -374,7 +377,7 @@ auto noto::default_value::trusted_expression(noto::trusted_sql_fragment fragment
return noto::default_value{fragment.value()}; return noto::default_value{fragment.value()};
} }
auto noto::default_value::sql() const -> std::string const& { auto noto::default_value::sql() const -> std::string const & {
return sql_; return sql_;
} }
@@ -382,7 +385,8 @@ auto noto::default_value::empty() const -> std::bool_t {
return sql_.empty(); return sql_.empty();
} }
noto::default_value::default_value(std::string sql) : sql_{std::move(sql)} {} noto::default_value::default_value(std::string sql)
: sql_{std::move(sql)} {}
auto noto::quote_identifier(std::string_view identifier) -> std::expected<std::string, modulus::error> { auto noto::quote_identifier(std::string_view identifier) -> std::expected<std::string, modulus::error> {
auto valid = validate_identifier(identifier); auto valid = validate_identifier(identifier);
@@ -402,7 +406,7 @@ auto noto::quote_identifier(std::string_view identifier) -> std::expected<std::s
return quoted; return quoted;
} }
auto noto::create_table_sql(noto::table_definition const& definition) -> std::expected<std::string, modulus::error> { auto noto::create_table_sql(noto::table_definition const &definition) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(definition.name); auto table = qualified_table(definition.name);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
@@ -410,7 +414,7 @@ auto noto::create_table_sql(noto::table_definition const& definition) -> std::ex
if (definition.columns.empty()) { if (definition.columns.empty()) {
return invalid_sql("table must define at least one column"); return invalid_sql("table must define at least one column");
} }
auto inline_primary_key_count = std::ranges::count_if(definition.columns, [](noto::column_definition const& column) { auto inline_primary_key_count = std::ranges::count_if(definition.columns, [](noto::column_definition const &column) {
return column.primary_key; return column.primary_key;
}); });
if (!definition.primary_key.empty() && inline_primary_key_count != 0) { if (!definition.primary_key.empty() && inline_primary_key_count != 0) {
@@ -420,7 +424,7 @@ auto noto::create_table_sql(noto::table_definition const& definition) -> std::ex
return invalid_sql("table must not define multiple column-level primary keys"); return invalid_sql("table must not define multiple column-level primary keys");
} }
auto definitions = std::vector<std::string>{}; auto definitions = std::vector<std::string>{};
for (auto const& column_definition : definition.columns) { for (auto const &column_definition : definition.columns) {
auto column = column_sql(column_definition); auto column = column_sql(column_definition);
if (!column) { if (!column) {
return std::unexpected(column.error()); return std::unexpected(column.error());
@@ -434,21 +438,21 @@ auto noto::create_table_sql(noto::table_definition const& definition) -> std::ex
} }
definitions.push_back(std::move(*constraint)); definitions.push_back(std::move(*constraint));
} }
for (auto const& unique_constraint : definition.unique_constraints) { for (auto const &unique_constraint : definition.unique_constraints) {
auto constraint = unique_constraint_sql(unique_constraint); auto constraint = unique_constraint_sql(unique_constraint);
if (!constraint) { if (!constraint) {
return std::unexpected(constraint.error()); return std::unexpected(constraint.error());
} }
definitions.push_back(std::move(*constraint)); definitions.push_back(std::move(*constraint));
} }
for (auto const& check_constraint : definition.check_constraints) { for (auto const &check_constraint : definition.check_constraints) {
auto constraint = check_constraint_sql(check_constraint); auto constraint = check_constraint_sql(check_constraint);
if (!constraint) { if (!constraint) {
return std::unexpected(constraint.error()); return std::unexpected(constraint.error());
} }
definitions.push_back(std::move(*constraint)); definitions.push_back(std::move(*constraint));
} }
for (auto const& foreign_key : definition.foreign_keys) { for (auto const &foreign_key : definition.foreign_keys) {
auto constraint = foreign_key_sql(foreign_key); auto constraint = foreign_key_sql(foreign_key);
if (!constraint) { if (!constraint) {
return std::unexpected(constraint.error()); return std::unexpected(constraint.error());
@@ -468,7 +472,7 @@ auto noto::create_table_sql(noto::table_definition const& definition) -> std::ex
return sql.str(); return sql.str();
} }
auto noto::drop_table_sql(noto::table_name const& table, std::bool_t if_exists) -> std::expected<std::string, modulus::error> { auto noto::drop_table_sql(noto::table_name const &table, std::bool_t if_exists) -> std::expected<std::string, modulus::error> {
auto name = qualified_table(table); auto name = qualified_table(table);
if (!name) { if (!name) {
return std::unexpected(name.error()); return std::unexpected(name.error());
@@ -476,7 +480,7 @@ auto noto::drop_table_sql(noto::table_name const& table, std::bool_t if_exists)
return std::string{"DROP TABLE "} + (if_exists ? "IF EXISTS " : "") + *name; return std::string{"DROP TABLE "} + (if_exists ? "IF EXISTS " : "") + *name;
} }
auto noto::create_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error> { auto noto::create_index_sql(noto::index_definition const &definition) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(definition.table); auto table = qualified_table(definition.table);
if (!table) { if (!table) {
return std::unexpected(table.error()); return std::unexpected(table.error());
@@ -503,7 +507,7 @@ auto noto::create_index_sql(noto::index_definition const& definition) -> std::ex
return sql.str(); return sql.str();
} }
auto noto::drop_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error> { auto noto::drop_index_sql(noto::index_definition const &definition) -> std::expected<std::string, modulus::error> {
auto index = noto::quote_identifier(definition.name); auto index = noto::quote_identifier(definition.name);
if (!index) { if (!index) {
return std::unexpected(index.error()); return std::unexpected(index.error());
+8 -4
View File
@@ -14,6 +14,7 @@
#include <noto/detail/core.hpp> #include <noto/detail/core.hpp>
namespace { namespace {
auto inactive_transaction() -> std::expected<void, modulus::error> { auto inactive_transaction() -> std::expected<void, modulus::error> {
@@ -24,7 +25,7 @@ auto async_status(std::expected<void, modulus::error> status) -> boost::asio::aw
co_return status; co_return status;
} }
auto finish_started_transaction(std::shared_ptr<noto::detail::transaction_state>& state, noto::detail::transaction_result const& result) -> void { auto finish_started_transaction(std::shared_ptr<noto::detail::transaction_state> &state, noto::detail::transaction_result const &result) -> void {
if (result.status) { if (result.status) {
state->core.reset(); state->core.reset();
state->finish(); state->finish();
@@ -76,7 +77,8 @@ auto async_rollback_impl(std::shared_ptr<noto::detail::transaction_state> state)
} }
noto::transaction::transaction(noto::transaction&& other) noexcept : state_{std::move(other.state_)} {} noto::transaction::transaction(noto::transaction &&other) noexcept
: state_{std::move(other.state_)} {}
noto::transaction::~transaction() { noto::transaction::~transaction() {
if (!state_) { if (!state_) {
@@ -106,6 +108,8 @@ auto noto::transaction::async_rollback() -> boost::asio::awaitable<std::expected
return async_rollback_impl(state_); return async_rollback_impl(state_);
} }
noto::transaction::transaction(std::shared_ptr<noto::detail::transaction_state> state) : state_{std::move(state)} {} noto::transaction::transaction(std::shared_ptr<noto::detail::transaction_state> state)
: state_{std::move(state)} {}
noto::transaction::transaction(std::shared_ptr<noto::detail::connection_core> core, std::function<void()> on_finish) : state_{std::make_shared<noto::detail::transaction_state>(std::move(core), std::move(on_finish))} {} noto::transaction::transaction(std::shared_ptr<noto::detail::connection_core> core, std::function<void()> on_finish)
: state_{std::make_shared<noto::detail::transaction_state>(std::move(core), std::move(on_finish))} {}
+28 -132
View File
@@ -34,6 +34,7 @@
#include <noto/schema.hpp> #include <noto/schema.hpp>
#include <noto/transaction.hpp> #include <noto/transaction.hpp>
BOOST_AUTO_TEST_CASE(noto_asserts_follow_build_type) { BOOST_AUTO_TEST_CASE(noto_asserts_follow_build_type) {
#if defined(NOTO_EXPECT_NDEBUG) && !defined(NDEBUG) #if defined(NOTO_EXPECT_NDEBUG) && !defined(NDEBUG)
BOOST_FAIL("release-style builds must define NDEBUG"); BOOST_FAIL("release-style builds must define NDEBUG");
@@ -43,11 +44,10 @@ BOOST_AUTO_TEST_CASE(noto_asserts_follow_build_type) {
#endif #endif
} }
namespace { namespace {
template <typename AWAITABLE> template <typename AWAITABLE>
auto run(boost::asio::io_context& io, AWAITABLE&& awaitable) { auto run(boost::asio::io_context &io, AWAITABLE &&awaitable) {
auto future = boost::asio::co_spawn(io, std::forward<AWAITABLE>(awaitable), boost::asio::use_future); auto future = boost::asio::co_spawn(io, std::forward<AWAITABLE>(awaitable), boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
@@ -59,24 +59,24 @@ auto temp_database_path(std::string_view name) -> std::string {
return path.string(); return path.string();
} }
auto remove_database_files(std::string const& path) -> void { auto remove_database_files(std::string const &path) -> void {
std::filesystem::remove(path); std::filesystem::remove(path);
std::filesystem::remove(path + "-wal"); std::filesystem::remove(path + "-wal");
std::filesystem::remove(path + "-shm"); std::filesystem::remove(path + "-shm");
std::filesystem::remove(path + "-journal"); std::filesystem::remove(path + "-journal");
} }
auto text_value(noto::sqlite_value const& value) -> std::string { auto text_value(noto::sqlite_value const &value) -> std::string {
auto text = std::get_if<std::string>(&value.value); auto text = std::get_if<std::string>(&value.value);
return text == nullptr ? std::string{} : *text; return text == nullptr ? std::string{} : *text;
} }
auto int_value(noto::sqlite_value const& value) -> std::int64_t { auto int_value(noto::sqlite_value const &value) -> std::int64_t {
auto integer = std::get_if<std::int64_t>(&value.value); auto integer = std::get_if<std::int64_t>(&value.value);
return integer == nullptr ? 0 : *integer; return integer == nullptr ? 0 : *integer;
} }
auto execute_status(noto::connection& connection, std::string sql) -> boost::asio::awaitable<std::expected<void, modulus::error>> { auto execute_status(noto::connection &connection, std::string sql) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto result = co_await connection.async_execute(std::move(sql)); auto result = co_await connection.async_execute(std::move(sql));
if (!result) { if (!result) {
co_return std::unexpected(result.error()); co_return std::unexpected(result.error());
@@ -84,15 +84,15 @@ auto execute_status(noto::connection& connection, std::string sql) -> boost::asi
co_return std::expected<void, modulus::error>{}; co_return std::expected<void, modulus::error>{};
} }
auto sqlite_exec_ok(sqlite3* database, std::string const& sql) -> std::bool_t { auto sqlite_exec_ok(sqlite3 *database, std::string const &sql) -> std::bool_t {
auto* error = static_cast<std::char_t*>(nullptr); auto *error = static_cast<std::char_t *>(nullptr);
auto code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, &error); auto code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, &error);
sqlite3_free(error); sqlite3_free(error);
return code == SQLITE_OK; return code == SQLITE_OK;
} }
auto create_sqlite_database(std::string const& path, std::string const& sql) -> std::bool_t { auto create_sqlite_database(std::string const &path, std::string const &sql) -> std::bool_t {
auto* database = static_cast<sqlite3*>(nullptr); auto *database = static_cast<sqlite3 *>(nullptr);
if (sqlite3_open_v2(path.c_str(), &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) { if (sqlite3_open_v2(path.c_str(), &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) {
if (database != nullptr) { if (database != nullptr) {
sqlite3_close(database); sqlite3_close(database);
@@ -103,7 +103,7 @@ auto create_sqlite_database(std::string const& path, std::string const& sql) ->
return sqlite3_close(database) == SQLITE_OK && ok; return sqlite3_close(database) == SQLITE_OK && ok;
} }
auto write_file(std::string const& path, std::string_view text) -> void { auto write_file(std::string const &path, std::string_view text) -> void {
auto output = std::ofstream{ auto output = std::ofstream{
path, path,
std::ios::binary | std::ios::trunc, std::ios::binary | std::ios::trunc,
@@ -111,7 +111,7 @@ auto write_file(std::string const& path, std::string_view text) -> void {
output << text; output << text;
} }
auto read_file(std::string const& path) -> std::string { auto read_file(std::string const &path) -> std::string {
auto input = std::ifstream{ auto input = std::ifstream{
path, path,
std::ios::binary, std::ios::binary,
@@ -121,8 +121,8 @@ auto read_file(std::string const& path) -> std::string {
return output.str(); return output.str();
} }
auto sqlite_scalar_int64(sqlite3* database, std::string const& sql) -> std::int64_t { auto sqlite_scalar_int64(sqlite3 *database, std::string const &sql) -> std::int64_t {
auto* statement = static_cast<sqlite3_stmt*>(nullptr); auto *statement = static_cast<sqlite3_stmt *>(nullptr);
if (sqlite3_prepare_v2(database, sql.c_str(), -1, &statement, nullptr) != SQLITE_OK) { if (sqlite3_prepare_v2(database, sql.c_str(), -1, &statement, nullptr) != SQLITE_OK) {
return 0; return 0;
} }
@@ -146,7 +146,6 @@ BOOST_AUTO_TEST_CASE(noto_status_defaults_to_ok) {
BOOST_AUTO_TEST_CASE(noto_result_carries_failure_status) { BOOST_AUTO_TEST_CASE(noto_result_carries_failure_status) {
auto ok = std::expected<void, modulus::error>{}; auto ok = std::expected<void, modulus::error>{};
BOOST_TEST(static_cast<std::bool_t>(ok.has_value())); BOOST_TEST(static_cast<std::bool_t>(ok.has_value()));
auto result = noto::detail::fail<std::int32_t>(noto::detail::error_invalid_argument, "bad value"); auto result = noto::detail::fail<std::int32_t>(noto::detail::error_invalid_argument, "bad value");
BOOST_REQUIRE(!result); BOOST_REQUIRE(!result);
BOOST_TEST(static_cast<std::int32_t>(result.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(result.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
@@ -163,10 +162,8 @@ BOOST_AUTO_TEST_CASE(noto_status_maps_sqlite_errors) {
BOOST_AUTO_TEST_CASE(noto_config_validates_and_parses_json) { BOOST_AUTO_TEST_CASE(noto_config_validates_and_parses_json) {
auto config = noto::connection_config{}; auto config = noto::connection_config{};
BOOST_TEST(static_cast<std::bool_t>(noto::validate(config))); BOOST_TEST(static_cast<std::bool_t>(noto::validate(config)));
config.filename = ""; config.filename = "";
BOOST_TEST(!static_cast<std::bool_t>(noto::validate(config))); BOOST_TEST(!static_cast<std::bool_t>(noto::validate(config)));
auto value = boost::json::parse(R"json({ auto value = boost::json::parse(R"json({
"databases": [ "databases": [
{ {
@@ -196,27 +193,19 @@ BOOST_AUTO_TEST_CASE(noto_config_validates_and_parses_json) {
BOOST_TEST(round_trip->at(0).name == "main"); BOOST_TEST(round_trip->at(0).name == "main");
BOOST_TEST(static_cast<std::int32_t>(round_trip->at(0).mode) == static_cast<std::int32_t>(noto::open_mode::MEMORY)); BOOST_TEST(static_cast<std::int32_t>(round_trip->at(0).mode) == static_cast<std::int32_t>(noto::open_mode::MEMORY));
BOOST_TEST(round_trip->at(0).pragmas.size() == 2U); BOOST_TEST(round_trip->at(0).pragmas.size() == 2U);
auto bad = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad","filename":":memory:","mode":"server"}]})json")); auto bad = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad","filename":":memory:","mode":"server"}]})json"));
BOOST_TEST(!static_cast<std::bool_t>(bad.has_value())); BOOST_TEST(!static_cast<std::bool_t>(bad.has_value()));
auto missing_file = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad"}]})json")); auto missing_file = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad"}]})json"));
BOOST_TEST(!static_cast<std::bool_t>(missing_file.has_value())); BOOST_TEST(!static_cast<std::bool_t>(missing_file.has_value()));
config.filename = ":memory:"; config.filename = ":memory:";
config.busy_timeout = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1}; config.busy_timeout = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1};
BOOST_TEST(!static_cast<std::bool_t>(noto::validate(config))); BOOST_TEST(!static_cast<std::bool_t>(noto::validate(config)));
auto oversized_timeout_json = std::string{R"json({"databases":[{"name":"bad","filename":":memory:","busy_timeout_ms":)json"} + std::to_string(static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1) + R"json(}]})json";
auto oversized_timeout_json = std::string{R"json({"databases":[{"name":"bad","filename":":memory:","busy_timeout_ms":)json"}
+ std::to_string(static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1)
+ R"json(}]})json";
auto oversized_timeout = noto::parse_database_configs(boost::json::parse(oversized_timeout_json)); auto oversized_timeout = noto::parse_database_configs(boost::json::parse(oversized_timeout_json));
BOOST_TEST(!static_cast<std::bool_t>(oversized_timeout.has_value())); BOOST_TEST(!static_cast<std::bool_t>(oversized_timeout.has_value()));
auto unknown_root = noto::parse_database_configs(boost::json::parse(R"json({"databases":[],"databasez":[]})json")); auto unknown_root = noto::parse_database_configs(boost::json::parse(R"json({"databases":[],"databasez":[]})json"));
BOOST_REQUIRE(!unknown_root); BOOST_REQUIRE(!unknown_root);
BOOST_TEST(unknown_root.error().describe().find("config root has unknown key: databasez") != std::string::npos); BOOST_TEST(unknown_root.error().describe().find("config root has unknown key: databasez") != std::string::npos);
auto unknown_database = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad","filename":":memory:","busy_timout_ms":250}]})json")); auto unknown_database = noto::parse_database_configs(boost::json::parse(R"json({"databases":[{"name":"bad","filename":":memory:","busy_timout_ms":250}]})json"));
BOOST_REQUIRE(!unknown_database); BOOST_REQUIRE(!unknown_database);
BOOST_TEST(unknown_database.error().describe().find("database has unknown key: busy_timout_ms") != std::string::npos); BOOST_TEST(unknown_database.error().describe().find("database has unknown key: busy_timout_ms") != std::string::npos);
@@ -226,7 +215,6 @@ BOOST_AUTO_TEST_CASE(noto_schema_quotes_identifiers_and_builds_sqlite_table_sql)
auto quoted = noto::quote_identifier("with\"quote"); auto quoted = noto::quote_identifier("with\"quote");
BOOST_REQUIRE(quoted); BOOST_REQUIRE(quoted);
BOOST_TEST(*quoted == "\"with\"\"quote\""); BOOST_TEST(*quoted == "\"with\"\"quote\"");
auto email_type = noto::column_type::varchar(255); auto email_type = noto::column_type::varchar(255);
BOOST_REQUIRE(email_type); BOOST_REQUIRE(email_type);
auto sql = noto::create_table_sql({ auto sql = noto::create_table_sql({
@@ -257,7 +245,6 @@ BOOST_AUTO_TEST_CASE(noto_schema_quotes_identifiers_and_builds_sqlite_table_sql)
.strict = true, .strict = true,
}, },
}); });
BOOST_REQUIRE(sql); BOOST_REQUIRE(sql);
BOOST_TEST(*sql == "CREATE TABLE IF NOT EXISTS \"users\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"email\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) STRICT"); BOOST_TEST(*sql == "CREATE TABLE IF NOT EXISTS \"users\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"email\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) STRICT");
} }
@@ -335,7 +322,6 @@ BOOST_AUTO_TEST_CASE(noto_schema_builds_constraints_generated_columns_and_option
.without_rowid = true, .without_rowid = true,
}, },
}); });
BOOST_REQUIRE(sql); BOOST_REQUIRE(sql);
BOOST_TEST(*sql == "CREATE TABLE IF NOT EXISTS \"accounts\" (\"tenant_id\" INTEGER NOT NULL, \"id\" INTEGER NOT NULL, \"email\" TEXT NOT NULL, \"email_key\" TEXT GENERATED ALWAYS AS (lower(\"email\")) STORED NOT NULL, PRIMARY KEY (\"tenant_id\", \"id\"), CONSTRAINT \"uq_accounts_email\" UNIQUE (\"tenant_id\", \"email_key\"), CONSTRAINT \"ck_accounts_email\" CHECK (\"email\" <> ''), CONSTRAINT \"fk_accounts_tenant\" FOREIGN KEY (\"tenant_id\") REFERENCES \"tenants\" (\"id\") ON DELETE CASCADE) WITHOUT ROWID"); BOOST_TEST(*sql == "CREATE TABLE IF NOT EXISTS \"accounts\" (\"tenant_id\" INTEGER NOT NULL, \"id\" INTEGER NOT NULL, \"email\" TEXT NOT NULL, \"email_key\" TEXT GENERATED ALWAYS AS (lower(\"email\")) STORED NOT NULL, PRIMARY KEY (\"tenant_id\", \"id\"), CONSTRAINT \"uq_accounts_email\" UNIQUE (\"tenant_id\", \"email_key\"), CONSTRAINT \"ck_accounts_email\" CHECK (\"email\" <> ''), CONSTRAINT \"fk_accounts_tenant\" FOREIGN KEY (\"tenant_id\") REFERENCES \"tenants\" (\"id\") ON DELETE CASCADE) WITHOUT ROWID");
} }
@@ -346,14 +332,11 @@ BOOST_AUTO_TEST_CASE(noto_connection_executes_queries_and_prepared_statements) {
io.get_executor(), io.get_executor(),
noto::memory_connection_config("test"), noto::memory_connection_config("test"),
}; };
auto connected = run(io, connection.async_connect()); auto connected = run(io, connection.async_connect());
BOOST_TEST(static_cast<std::bool_t>(connected)); BOOST_TEST(static_cast<std::bool_t>(connected));
BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED)); BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED));
auto created = run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)")); auto created = run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)"));
BOOST_REQUIRE(created); BOOST_REQUIRE(created);
auto statement = run(io, connection.async_prepare("INSERT INTO items(value) VALUES(?1)")); auto statement = run(io, connection.async_prepare("INSERT INTO items(value) VALUES(?1)"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto inserted = run(io, statement->async_execute({ auto inserted = run(io, statement->async_execute({
@@ -361,14 +344,12 @@ BOOST_AUTO_TEST_CASE(noto_connection_executes_queries_and_prepared_statements) {
})); }));
BOOST_REQUIRE(inserted); BOOST_REQUIRE(inserted);
BOOST_TEST(inserted->changes == 1); BOOST_TEST(inserted->changes == 1);
auto selected = run(io, connection.async_execute("SELECT value FROM items WHERE id = ?1", { auto selected = run(io, connection.async_execute("SELECT value FROM items WHERE id = ?1", {
noto::sqlite_value{inserted->last_insert_rowid}, noto::sqlite_value{inserted->last_insert_rowid},
})); }));
BOOST_REQUIRE(selected); BOOST_REQUIRE(selected);
BOOST_REQUIRE(selected->rows.size() == 1U); BOOST_REQUIRE(selected->rows.size() == 1U);
BOOST_TEST(text_value(selected->rows.at(0).at(0)) == "alpha"); BOOST_TEST(text_value(selected->rows.at(0).at(0)) == "alpha");
auto finalized = run(io, statement->async_finalize()); auto finalized = run(io, statement->async_finalize());
BOOST_TEST(static_cast<std::bool_t>(finalized)); BOOST_TEST(static_cast<std::bool_t>(finalized));
auto closed = run(io, connection.async_close()); auto closed = run(io, connection.async_close());
@@ -382,14 +363,12 @@ BOOST_AUTO_TEST_CASE(noto_close_with_live_statement_is_recoverable) {
noto::memory_connection_config("live_statement"), noto::memory_connection_config("live_statement"),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
auto statement = run(io, connection.async_prepare("SELECT 1")); auto statement = run(io, connection.async_prepare("SELECT 1"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto blocked = run(io, connection.async_close()); auto blocked = run(io, connection.async_close());
BOOST_TEST(!static_cast<std::bool_t>(blocked)); BOOST_TEST(!static_cast<std::bool_t>(blocked));
BOOST_TEST(static_cast<std::int32_t>(blocked.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable)); BOOST_TEST(static_cast<std::int32_t>(blocked.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable));
BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED)); BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED));
BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize()))); BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize())));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
} }
@@ -403,14 +382,12 @@ BOOST_AUTO_TEST_CASE(noto_close_rolls_back_active_transaction) {
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
{ {
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('transient')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('transient')")));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
} }
auto reopened = noto::connection{ auto reopened = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(path), noto::file_connection_config(path),
@@ -425,33 +402,29 @@ BOOST_AUTO_TEST_CASE(noto_close_rolls_back_active_transaction) {
BOOST_AUTO_TEST_CASE(noto_async_methods_capture_state_from_temporary_wrappers) { BOOST_AUTO_TEST_CASE(noto_async_methods_capture_state_from_temporary_wrappers) {
auto io = boost::asio::io_context{}; auto io = boost::asio::io_context{};
auto connect_future = boost::asio::co_spawn( auto connect_future = boost::asio::co_spawn(
io, io,
noto::connection{ noto::connection{
io.get_executor(), io.get_executor(),
noto::memory_connection_config("temporary_connection"), noto::memory_connection_config("temporary_connection"),
}.async_connect(), }
boost::asio::use_future .async_connect(),
); boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
BOOST_TEST(static_cast<std::bool_t>(connect_future.get())); BOOST_TEST(static_cast<std::bool_t>(connect_future.get()));
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::memory_connection_config("temporary_wrappers"), noto::memory_connection_config("temporary_wrappers"),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
auto statement = run(io, connection.async_prepare("SELECT 1")); auto statement = run(io, connection.async_prepare("SELECT 1"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto finalize_future = boost::asio::co_spawn(io, noto::statement{std::move(*statement)}.async_finalize(), boost::asio::use_future); auto finalize_future = boost::asio::co_spawn(io, noto::statement{std::move(*statement)}.async_finalize(), boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
BOOST_TEST(static_cast<std::bool_t>(finalize_future.get())); BOOST_TEST(static_cast<std::bool_t>(finalize_future.get()));
auto rolled_back = run(io, connection.async_begin_transaction()); auto rolled_back = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(rolled_back); BOOST_REQUIRE(rolled_back);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('rolled back')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('rolled back')")));
@@ -459,7 +432,6 @@ BOOST_AUTO_TEST_CASE(noto_async_methods_capture_state_from_temporary_wrappers) {
io.run(); io.run();
io.restart(); io.restart();
BOOST_TEST(static_cast<std::bool_t>(rollback_future.get())); BOOST_TEST(static_cast<std::bool_t>(rollback_future.get()));
auto committed = run(io, connection.async_begin_transaction()); auto committed = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(committed); BOOST_REQUIRE(committed);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('committed')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('committed')")));
@@ -467,7 +439,6 @@ BOOST_AUTO_TEST_CASE(noto_async_methods_capture_state_from_temporary_wrappers) {
io.run(); io.run();
io.restart(); io.restart();
BOOST_TEST(static_cast<std::bool_t>(commit_future.get())); BOOST_TEST(static_cast<std::bool_t>(commit_future.get()));
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 1); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 1);
@@ -483,22 +454,18 @@ BOOST_AUTO_TEST_CASE(noto_detached_transaction_rolls_back_when_commit_cannot_sta
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value INTEGER NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value INTEGER NOT NULL)")));
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES(1), (2), (3), (4), (5)"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES(1), (2), (3), (4), (5)")));
auto options = noto::export_options{}; auto options = noto::export_options{};
options.rows_per_yield = 1; options.rows_per_yield = 1;
auto export_future = boost::asio::co_spawn(io, connection.async_export_sql_to_file(export_path, options), boost::asio::use_future); auto export_future = boost::asio::co_spawn(io, connection.async_export_sql_to_file(export_path, options), boost::asio::use_future);
auto commit_future = boost::asio::co_spawn(io, noto::transaction{std::move(*transaction)}.async_commit(), boost::asio::use_future); auto commit_future = boost::asio::co_spawn(io, noto::transaction{std::move(*transaction)}.async_commit(), boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
BOOST_TEST(static_cast<std::bool_t>(export_future.get())); BOOST_TEST(static_cast<std::bool_t>(export_future.get()));
auto commit = commit_future.get(); auto commit = commit_future.get();
BOOST_TEST(static_cast<std::int32_t>(commit.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable)); BOOST_TEST(static_cast<std::int32_t>(commit.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable));
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
@@ -513,12 +480,10 @@ BOOST_AUTO_TEST_CASE(noto_statement_destructor_cleanup_allows_close) {
noto::memory_connection_config("statement_destructor"), noto::memory_connection_config("statement_destructor"),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
{ {
auto statement = run(io, connection.async_prepare("SELECT 1")); auto statement = run(io, connection.async_prepare("SELECT 1"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
} }
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
} }
@@ -530,7 +495,6 @@ BOOST_AUTO_TEST_CASE(noto_transactions_commit_and_rollback) {
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
{ {
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
@@ -540,7 +504,6 @@ BOOST_AUTO_TEST_CASE(noto_transactions_commit_and_rollback) {
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
{ {
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
@@ -561,21 +524,17 @@ BOOST_AUTO_TEST_CASE(noto_failed_commit_remains_rollbackable) {
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE parents(id INTEGER PRIMARY KEY)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE parents(id INTEGER PRIMARY KEY)")));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE children(parent_id INTEGER NOT NULL, FOREIGN KEY(parent_id) REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE children(parent_id INTEGER NOT NULL, FOREIGN KEY(parent_id) REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)")));
auto transaction = run(io, connection.async_begin_transaction(noto::transaction_mode::DEFERRED)); auto transaction = run(io, connection.async_begin_transaction(noto::transaction_mode::DEFERRED));
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO children(parent_id) VALUES(42)"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO children(parent_id) VALUES(42)")));
auto committed = run(io, transaction->async_commit()); auto committed = run(io, transaction->async_commit());
BOOST_TEST(!static_cast<std::bool_t>(committed)); BOOST_TEST(!static_cast<std::bool_t>(committed));
BOOST_TEST(static_cast<std::int32_t>(committed.error().code()) == static_cast<std::int32_t>(noto::detail::error_constraint_violation)); BOOST_TEST(static_cast<std::int32_t>(committed.error().code()) == static_cast<std::int32_t>(noto::detail::error_constraint_violation));
BOOST_TEST(static_cast<std::bool_t>(transaction->active())); BOOST_TEST(static_cast<std::bool_t>(transaction->active()));
BOOST_TEST(static_cast<std::bool_t>(run(io, transaction->async_rollback()))); BOOST_TEST(static_cast<std::bool_t>(run(io, transaction->async_rollback())));
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM children")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM children"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
auto next = run(io, connection.async_begin_transaction()); auto next = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(next); BOOST_REQUIRE(next);
BOOST_TEST(static_cast<std::bool_t>(run(io, next->async_rollback()))); BOOST_TEST(static_cast<std::bool_t>(run(io, next->async_rollback())));
@@ -591,22 +550,18 @@ BOOST_AUTO_TEST_CASE(noto_detached_failed_commit_rolls_back) {
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE parents(id INTEGER PRIMARY KEY)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE parents(id INTEGER PRIMARY KEY)")));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE children(parent_id INTEGER NOT NULL, FOREIGN KEY(parent_id) REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE children(parent_id INTEGER NOT NULL, FOREIGN KEY(parent_id) REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)")));
auto transaction = run(io, connection.async_begin_transaction(noto::transaction_mode::DEFERRED)); auto transaction = run(io, connection.async_begin_transaction(noto::transaction_mode::DEFERRED));
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO children(parent_id) VALUES(42)"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO children(parent_id) VALUES(42)")));
auto commit_future = boost::asio::co_spawn(io, noto::transaction{std::move(*transaction)}.async_commit(), boost::asio::use_future); auto commit_future = boost::asio::co_spawn(io, noto::transaction{std::move(*transaction)}.async_commit(), boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
auto committed = commit_future.get(); auto committed = commit_future.get();
BOOST_TEST(!static_cast<std::bool_t>(committed)); BOOST_TEST(!static_cast<std::bool_t>(committed));
BOOST_TEST(static_cast<std::int32_t>(committed.error().code()) == static_cast<std::int32_t>(noto::detail::error_constraint_violation)); BOOST_TEST(static_cast<std::int32_t>(committed.error().code()) == static_cast<std::int32_t>(noto::detail::error_constraint_violation));
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM children")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM children"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
auto next = run(io, connection.async_begin_transaction()); auto next = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(next); BOOST_REQUIRE(next);
BOOST_TEST(static_cast<std::bool_t>(run(io, next->async_rollback()))); BOOST_TEST(static_cast<std::bool_t>(run(io, next->async_rollback())));
@@ -621,13 +576,11 @@ BOOST_AUTO_TEST_CASE(noto_abandoned_transaction_rolls_back_before_next_operation
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
{ {
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('abandoned')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('abandoned')")));
} }
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
@@ -642,8 +595,7 @@ BOOST_AUTO_TEST_CASE(noto_transaction_callback_exceptions_return_status_and_roll
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
auto status = run(io, connection.async_with_transaction(noto::transaction_mode::IMMEDIATE, [](noto::connection &active) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto status = run(io, connection.async_with_transaction(noto::transaction_mode::IMMEDIATE, [](noto::connection& active) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto inserted = co_await active.async_execute("INSERT INTO items(value) VALUES('temporary')"); auto inserted = co_await active.async_execute("INSERT INTO items(value) VALUES('temporary')");
if (!inserted) { if (!inserted) {
co_return std::unexpected(inserted.error()); co_return std::unexpected(inserted.error());
@@ -652,7 +604,6 @@ BOOST_AUTO_TEST_CASE(noto_transaction_callback_exceptions_return_status_and_roll
})); }));
BOOST_TEST(!static_cast<std::bool_t>(status)); BOOST_TEST(!static_cast<std::bool_t>(status));
BOOST_TEST(static_cast<std::int32_t>(status.error().code()) == static_cast<std::int32_t>(noto::detail::error_internal)); BOOST_TEST(static_cast<std::int32_t>(status.error().code()) == static_cast<std::int32_t>(noto::detail::error_internal));
auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items")); auto count = run(io, connection.async_execute("SELECT COUNT(*) FROM items"));
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 0);
@@ -667,19 +618,16 @@ BOOST_AUTO_TEST_CASE(noto_execute_options_limit_collected_results) {
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')")));
auto row_options = noto::execute_options{}; auto row_options = noto::execute_options{};
row_options.max_rows = 1; row_options.max_rows = 1;
auto too_many_rows = run(io, connection.async_execute("SELECT value FROM items ORDER BY value", {}, row_options)); auto too_many_rows = run(io, connection.async_execute("SELECT value FROM items ORDER BY value", {}, row_options));
BOOST_REQUIRE(!too_many_rows); BOOST_REQUIRE(!too_many_rows);
BOOST_TEST(static_cast<std::int32_t>(too_many_rows.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted)); BOOST_TEST(static_cast<std::int32_t>(too_many_rows.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted));
auto byte_options = noto::execute_options{}; auto byte_options = noto::execute_options{};
byte_options.max_result_bytes = 4; byte_options.max_result_bytes = 4;
auto too_many_bytes = run(io, connection.async_execute("SELECT value FROM items WHERE value = 'one'", {}, byte_options)); auto too_many_bytes = run(io, connection.async_execute("SELECT value FROM items WHERE value = 'one'", {}, byte_options));
BOOST_REQUIRE(!too_many_bytes); BOOST_REQUIRE(!too_many_bytes);
BOOST_TEST(static_cast<std::int32_t>(too_many_bytes.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted)); BOOST_TEST(static_cast<std::int32_t>(too_many_bytes.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted));
auto statement = run(io, connection.async_prepare("SELECT value FROM items ORDER BY value")); auto statement = run(io, connection.async_prepare("SELECT value FROM items ORDER BY value"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto statement_options = noto::execute_options{}; auto statement_options = noto::execute_options{};
@@ -700,16 +648,15 @@ BOOST_AUTO_TEST_CASE(noto_migrations_apply_validate_and_rollback) {
noto::memory_connection_config(), noto::memory_connection_config(),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
auto migrations = std::vector<noto::migration>{ auto migrations = std::vector<noto::migration>{
{ {
.id = "001_create_table", .id = "001_create_table",
.description = "create items", .description = "create items",
.checksum = "checksum-001", .checksum = "checksum-001",
.up = [](noto::connection& active) -> boost::asio::awaitable<std::expected<void, modulus::error>> { .up = [](noto::connection &active) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await execute_status(active, "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)"); co_return co_await execute_status(active, "CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)");
}, },
.down = [](noto::connection& active) -> boost::asio::awaitable<std::expected<void, modulus::error>> { .down = [](noto::connection &active) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await execute_status(active, "DROP TABLE IF EXISTS items"); co_return co_await execute_status(active, "DROP TABLE IF EXISTS items");
}, },
}, },
@@ -717,18 +664,15 @@ BOOST_AUTO_TEST_CASE(noto_migrations_apply_validate_and_rollback) {
auto migrator = noto::migrator{connection}; auto migrator = noto::migrator{connection};
BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_migrate(migrations)))); BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_migrate(migrations))));
BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_migrate(migrations)))); BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_migrate(migrations))));
auto changed = migrations; auto changed = migrations;
changed.at(0).checksum = "changed"; changed.at(0).checksum = "changed";
auto mismatch = run(io, migrator.async_migrate(changed)); auto mismatch = run(io, migrator.async_migrate(changed));
BOOST_TEST(!static_cast<std::bool_t>(mismatch)); BOOST_TEST(!static_cast<std::bool_t>(mismatch));
BOOST_TEST(static_cast<std::int32_t>(mismatch.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(mismatch.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
auto records = run(io, migrator.async_applied_migrations()); auto records = run(io, migrator.async_applied_migrations());
BOOST_REQUIRE(records); BOOST_REQUIRE(records);
BOOST_TEST(records->size() == 1U); BOOST_TEST(records->size() == 1U);
BOOST_TEST(records->at(0).id == "001_create_table"); BOOST_TEST(records->at(0).id == "001_create_table");
BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_rollback(migrations)))); BOOST_TEST(static_cast<std::bool_t>(run(io, migrator.async_rollback(migrations))));
auto missing = run(io, connection.async_execute("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'items'")); auto missing = run(io, connection.async_execute("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'items'"));
BOOST_REQUIRE(missing); BOOST_REQUIRE(missing);
@@ -742,7 +686,6 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
auto failed_backup_path = temp_database_path("failed_backup"); auto failed_backup_path = temp_database_path("failed_backup");
auto oversized_retry_path = temp_database_path("oversized_retry"); auto oversized_retry_path = temp_database_path("oversized_retry");
auto export_path = temp_database_path("export_sql"); auto export_path = temp_database_path("export_sql");
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(source_path), noto::file_connection_config(source_path),
@@ -759,14 +702,12 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
auto busy_timeout = run(io, connection.async_busy_timeout()); auto busy_timeout = run(io, connection.async_busy_timeout());
BOOST_REQUIRE(busy_timeout); BOOST_REQUIRE(busy_timeout);
BOOST_TEST(*busy_timeout == std::chrono::milliseconds{37}); BOOST_TEST(*busy_timeout == std::chrono::milliseconds{37});
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY, value TEXT NOT NULL)")));
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')")));
auto integrity = run(io, connection.async_integrity_check()); auto integrity = run(io, connection.async_integrity_check());
BOOST_REQUIRE(integrity); BOOST_REQUIRE(integrity);
BOOST_REQUIRE(integrity->size() == 1U); BOOST_REQUIRE(integrity->size() == 1U);
BOOST_TEST(integrity->at(0) == "ok"); BOOST_TEST(integrity->at(0) == "ok");
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_backup_to_file(backup_path)))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_backup_to_file(backup_path))));
auto exported = run(io, connection.async_export_sql()); auto exported = run(io, connection.async_export_sql());
BOOST_REQUIRE(exported); BOOST_REQUIRE(exported);
@@ -776,8 +717,7 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
auto limited_export = run(io, connection.async_export_sql(limited_options)); auto limited_export = run(io, connection.async_export_sql(limited_options));
BOOST_REQUIRE(!limited_export); BOOST_REQUIRE(!limited_export);
BOOST_TEST(static_cast<std::int32_t>(limited_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted)); BOOST_TEST(static_cast<std::int32_t>(limited_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted));
auto *preserved = static_cast<sqlite3 *>(nullptr);
auto* preserved = static_cast<sqlite3*>(nullptr);
BOOST_REQUIRE(sqlite3_open_v2(failed_backup_path.c_str(), &preserved, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open_v2(failed_backup_path.c_str(), &preserved, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK);
BOOST_REQUIRE(sqlite_exec_ok(preserved, "CREATE TABLE preserved(value INTEGER); INSERT INTO preserved(value) VALUES(42)")); BOOST_REQUIRE(sqlite_exec_ok(preserved, "CREATE TABLE preserved(value INTEGER); INSERT INTO preserved(value) VALUES(42)"));
sqlite3_close(preserved); sqlite3_close(preserved);
@@ -789,17 +729,14 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
BOOST_REQUIRE(sqlite3_open_v2(failed_backup_path.c_str(), &preserved, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open_v2(failed_backup_path.c_str(), &preserved, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK);
BOOST_TEST(sqlite_scalar_int64(preserved, "SELECT value FROM preserved") == 42); BOOST_TEST(sqlite_scalar_int64(preserved, "SELECT value FROM preserved") == 42);
sqlite3_close(preserved); sqlite3_close(preserved);
auto oversized_retry = noto::backup_options{}; auto oversized_retry = noto::backup_options{};
oversized_retry.retry_delay = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1}; oversized_retry.retry_delay = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1};
auto failed_delay = run(io, connection.async_backup_to_file(oversized_retry_path, oversized_retry)); auto failed_delay = run(io, connection.async_backup_to_file(oversized_retry_path, oversized_retry));
BOOST_TEST(static_cast<std::int32_t>(failed_delay.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(failed_delay.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
oversized_retry = noto::backup_options{}; oversized_retry = noto::backup_options{};
oversized_retry.retry_timeout = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1}; oversized_retry.retry_timeout = std::chrono::milliseconds{static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1};
auto failed_timeout = run(io, connection.async_backup_to_file(oversized_retry_path, oversized_retry)); auto failed_timeout = run(io, connection.async_backup_to_file(oversized_retry_path, oversized_retry));
BOOST_TEST(static_cast<std::int32_t>(failed_timeout.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(failed_timeout.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
write_file(export_path, "preserved export"); write_file(export_path, "preserved export");
auto limited_file_options = noto::export_options{}; auto limited_file_options = noto::export_options{};
limited_file_options.max_output_bytes = 16; limited_file_options.max_output_bytes = 16;
@@ -807,11 +744,9 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
BOOST_REQUIRE(!limited_file_export); BOOST_REQUIRE(!limited_file_export);
BOOST_TEST(static_cast<std::int32_t>(limited_file_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted)); BOOST_TEST(static_cast<std::int32_t>(limited_file_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_resource_exhausted));
BOOST_TEST(read_file(export_path) == "preserved export"); BOOST_TEST(read_file(export_path) == "preserved export");
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_export_sql_to_file(export_path)))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_export_sql_to_file(export_path))));
BOOST_TEST(static_cast<std::bool_t>(std::filesystem::exists(export_path))); BOOST_TEST(static_cast<std::bool_t>(std::filesystem::exists(export_path)));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
auto restored = noto::connection{ auto restored = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(backup_path), noto::file_connection_config(backup_path),
@@ -821,7 +756,6 @@ BOOST_AUTO_TEST_CASE(noto_backup_export_integrity_and_pragmas_work) {
BOOST_REQUIRE(count); BOOST_REQUIRE(count);
BOOST_TEST(int_value(count->rows.at(0).at(0)) == 2); BOOST_TEST(int_value(count->rows.at(0).at(0)) == 2);
BOOST_TEST(static_cast<std::bool_t>(run(io, restored.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, restored.async_close())));
std::filesystem::remove(source_path); std::filesystem::remove(source_path);
std::filesystem::remove(backup_path); std::filesystem::remove(backup_path);
std::filesystem::remove(failed_backup_path); std::filesystem::remove(failed_backup_path);
@@ -840,19 +774,16 @@ BOOST_AUTO_TEST_CASE(noto_backup_and_export_reject_live_database_destinations) {
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('safe')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('safe')")));
auto self_backup = run(io, connection.async_backup_to_file(source_path)); auto self_backup = run(io, connection.async_backup_to_file(source_path));
BOOST_TEST(static_cast<std::int32_t>(self_backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(self_backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
auto self_export = run(io, connection.async_export_sql_to_file(source_path)); auto self_export = run(io, connection.async_export_sql_to_file(source_path));
BOOST_TEST(static_cast<std::int32_t>(self_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(self_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
for (auto suffix : std::array<std::string_view, 3>{"-wal", "-shm", "-journal"}) { for (auto suffix : std::array<std::string_view, 3>{"-wal", "-shm", "-journal"}) {
auto backup = run(io, connection.async_backup_to_file(source_path + std::string{suffix})); auto backup = run(io, connection.async_backup_to_file(source_path + std::string{suffix}));
BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
auto exported = run(io, connection.async_export_sql_to_file(source_path + std::string{suffix})); auto exported = run(io, connection.async_export_sql_to_file(source_path + std::string{suffix}));
BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
} }
auto error = std::error_code{}; auto error = std::error_code{};
std::filesystem::create_symlink(source_path, link_path, error); std::filesystem::create_symlink(source_path, link_path, error);
if (!error) { if (!error) {
@@ -861,7 +792,6 @@ BOOST_AUTO_TEST_CASE(noto_backup_and_export_reject_live_database_destinations) {
auto link_export = run(io, connection.async_export_sql_to_file(link_path)); auto link_export = run(io, connection.async_export_sql_to_file(link_path));
BOOST_TEST(static_cast<std::int32_t>(link_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(link_export.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
} }
auto rows = run(io, connection.async_execute("SELECT value FROM items")); auto rows = run(io, connection.async_execute("SELECT value FROM items"));
BOOST_REQUIRE(rows); BOOST_REQUIRE(rows);
BOOST_REQUIRE(rows->rows.size() == 1U); BOOST_REQUIRE(rows->rows.size() == 1U);
@@ -875,9 +805,7 @@ BOOST_AUTO_TEST_CASE(noto_restore_to_file_is_atomic_and_reopens_connection) {
auto io = boost::asio::io_context{}; auto io = boost::asio::io_context{};
auto source_path = temp_database_path("restore_source"); auto source_path = temp_database_path("restore_source");
auto target_path = temp_database_path("restore_target"); auto target_path = temp_database_path("restore_target");
BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')")); BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')"));
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(target_path), noto::file_connection_config(target_path),
@@ -890,13 +818,11 @@ BOOST_AUTO_TEST_CASE(noto_restore_to_file_is_atomic_and_reopens_connection) {
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_set_busy_timeout(std::chrono::milliseconds{37})))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_set_busy_timeout(std::chrono::milliseconds{37}))));
write_file(target_path + "-journal", "stale rollback journal"); write_file(target_path + "-journal", "stale rollback journal");
BOOST_TEST(static_cast<std::bool_t>(std::filesystem::exists(target_path + "-journal"))); BOOST_TEST(static_cast<std::bool_t>(std::filesystem::exists(target_path + "-journal")));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_restore_from_file(source_path)))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_restore_from_file(source_path))));
BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED)); BOOST_TEST(static_cast<std::int32_t>(connection.state()) == static_cast<std::int32_t>(noto::connection_state::CONNECTED));
auto timeout = run(io, connection.async_busy_timeout()); auto timeout = run(io, connection.async_busy_timeout());
BOOST_REQUIRE(timeout); BOOST_REQUIRE(timeout);
BOOST_TEST(*timeout == std::chrono::milliseconds{37}); BOOST_TEST(*timeout == std::chrono::milliseconds{37});
auto rows = run(io, connection.async_execute("SELECT value FROM items")); auto rows = run(io, connection.async_execute("SELECT value FROM items"));
BOOST_REQUIRE(rows); BOOST_REQUIRE(rows);
BOOST_REQUIRE(rows->rows.size() == 1U); BOOST_REQUIRE(rows->rows.size() == 1U);
@@ -905,12 +831,10 @@ BOOST_AUTO_TEST_CASE(noto_restore_to_file_is_atomic_and_reopens_connection) {
BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-wal"))); BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-wal")));
BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-shm"))); BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-shm")));
BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-journal"))); BOOST_TEST(!static_cast<std::bool_t>(std::filesystem::exists(target_path + "-journal")));
auto *restored = static_cast<sqlite3 *>(nullptr);
auto* restored = static_cast<sqlite3*>(nullptr);
BOOST_REQUIRE(sqlite3_open_v2(target_path.c_str(), &restored, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open_v2(target_path.c_str(), &restored, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK);
BOOST_TEST(sqlite_scalar_int64(restored, "SELECT COUNT(*) FROM items WHERE value = 'new'") == 1); BOOST_TEST(sqlite_scalar_int64(restored, "SELECT COUNT(*) FROM items WHERE value = 'new'") == 1);
sqlite3_close(restored); sqlite3_close(restored);
remove_database_files(source_path); remove_database_files(source_path);
remove_database_files(target_path); remove_database_files(target_path);
} }
@@ -919,10 +843,8 @@ BOOST_AUTO_TEST_CASE(noto_failed_restore_leaves_existing_database_intact) {
auto io = boost::asio::io_context{}; auto io = boost::asio::io_context{};
auto source_path = temp_database_path("restore_failure_source"); auto source_path = temp_database_path("restore_failure_source");
auto target_path = temp_database_path("restore_failure_target"); auto target_path = temp_database_path("restore_failure_target");
BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')")); BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')"));
BOOST_REQUIRE(create_sqlite_database(target_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('old')")); BOOST_REQUIRE(create_sqlite_database(target_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('old')"));
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(target_path), noto::file_connection_config(target_path),
@@ -932,13 +854,11 @@ BOOST_AUTO_TEST_CASE(noto_failed_restore_leaves_existing_database_intact) {
options.source_name = "missing"; options.source_name = "missing";
auto restored = run(io, connection.async_restore_from_file(source_path, options)); auto restored = run(io, connection.async_restore_from_file(source_path, options));
BOOST_TEST(!static_cast<std::bool_t>(restored)); BOOST_TEST(!static_cast<std::bool_t>(restored));
auto rows = run(io, connection.async_execute("SELECT value FROM items")); auto rows = run(io, connection.async_execute("SELECT value FROM items"));
BOOST_REQUIRE(rows); BOOST_REQUIRE(rows);
BOOST_REQUIRE(rows->rows.size() == 1U); BOOST_REQUIRE(rows->rows.size() == 1U);
BOOST_TEST(text_value(rows->rows.at(0).at(0)) == "old"); BOOST_TEST(text_value(rows->rows.at(0).at(0)) == "old");
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
remove_database_files(source_path); remove_database_files(source_path);
remove_database_files(target_path); remove_database_files(target_path);
} }
@@ -947,10 +867,8 @@ BOOST_AUTO_TEST_CASE(noto_restore_rejects_unsupported_targets_and_active_state)
auto io = boost::asio::io_context{}; auto io = boost::asio::io_context{};
auto source_path = temp_database_path("restore_reject_source"); auto source_path = temp_database_path("restore_reject_source");
auto target_path = temp_database_path("restore_reject_target"); auto target_path = temp_database_path("restore_reject_target");
BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')")); BOOST_REQUIRE(create_sqlite_database(source_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('new')"));
BOOST_REQUIRE(create_sqlite_database(target_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('old')")); BOOST_REQUIRE(create_sqlite_database(target_path, "CREATE TABLE items(value TEXT NOT NULL); INSERT INTO items(value) VALUES('old')"));
auto memory = noto::connection{ auto memory = noto::connection{
io.get_executor(), io.get_executor(),
noto::memory_connection_config("restore_reject_memory"), noto::memory_connection_config("restore_reject_memory"),
@@ -959,31 +877,26 @@ BOOST_AUTO_TEST_CASE(noto_restore_rejects_unsupported_targets_and_active_state)
auto memory_restore = run(io, memory.async_restore_from_file(source_path)); auto memory_restore = run(io, memory.async_restore_from_file(source_path));
BOOST_TEST(static_cast<std::int32_t>(memory_restore.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(memory_restore.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
BOOST_TEST(static_cast<std::bool_t>(run(io, memory.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, memory.async_close())));
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(target_path), noto::file_connection_config(target_path),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
auto options = noto::backup_options{}; auto options = noto::backup_options{};
options.destination_name = "attached"; options.destination_name = "attached";
auto non_main = run(io, connection.async_restore_from_file(source_path, options)); auto non_main = run(io, connection.async_restore_from_file(source_path, options));
BOOST_TEST(static_cast<std::int32_t>(non_main.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(non_main.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
auto transaction = run(io, connection.async_begin_transaction()); auto transaction = run(io, connection.async_begin_transaction());
BOOST_REQUIRE(transaction); BOOST_REQUIRE(transaction);
auto active_transaction = run(io, connection.async_restore_from_file(source_path)); auto active_transaction = run(io, connection.async_restore_from_file(source_path));
BOOST_TEST(static_cast<std::int32_t>(active_transaction.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable)); BOOST_TEST(static_cast<std::int32_t>(active_transaction.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable));
BOOST_TEST(static_cast<std::bool_t>(run(io, transaction->async_rollback()))); BOOST_TEST(static_cast<std::bool_t>(run(io, transaction->async_rollback())));
auto statement = run(io, connection.async_prepare("SELECT value FROM items")); auto statement = run(io, connection.async_prepare("SELECT value FROM items"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto live_statement = run(io, connection.async_restore_from_file(source_path)); auto live_statement = run(io, connection.async_restore_from_file(source_path));
BOOST_TEST(static_cast<std::int32_t>(live_statement.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable)); BOOST_TEST(static_cast<std::int32_t>(live_statement.error().code()) == static_cast<std::int32_t>(noto::detail::error_unavailable));
BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize()))); BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize())));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
remove_database_files(source_path); remove_database_files(source_path);
remove_database_files(target_path); remove_database_files(target_path);
} }
@@ -992,7 +905,6 @@ BOOST_AUTO_TEST_CASE(noto_backup_lock_timeout_and_statement_finalize_retry_work)
auto io = boost::asio::io_context{}; auto io = boost::asio::io_context{};
auto source_path = temp_database_path("locked_source"); auto source_path = temp_database_path("locked_source");
auto backup_path = temp_database_path("locked_backup"); auto backup_path = temp_database_path("locked_backup");
auto connection = noto::connection{ auto connection = noto::connection{
io.get_executor(), io.get_executor(),
noto::file_connection_config(source_path), noto::file_connection_config(source_path),
@@ -1002,25 +914,20 @@ BOOST_AUTO_TEST_CASE(noto_backup_lock_timeout_and_statement_finalize_retry_work)
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one')")));
auto statement = run(io, connection.async_prepare("SELECT value FROM items")); auto statement = run(io, connection.async_prepare("SELECT value FROM items"));
BOOST_REQUIRE(statement); BOOST_REQUIRE(statement);
auto *blocker = static_cast<sqlite3 *>(nullptr);
auto* blocker = static_cast<sqlite3*>(nullptr);
BOOST_REQUIRE(sqlite3_open_v2(source_path.c_str(), &blocker, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open_v2(source_path.c_str(), &blocker, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nullptr) == SQLITE_OK);
BOOST_REQUIRE(sqlite_exec_ok(blocker, "BEGIN EXCLUSIVE")); BOOST_REQUIRE(sqlite_exec_ok(blocker, "BEGIN EXCLUSIVE"));
auto options = noto::backup_options{}; auto options = noto::backup_options{};
options.pages_per_step = 1; options.pages_per_step = 1;
options.retry_delay = std::chrono::milliseconds{1}; options.retry_delay = std::chrono::milliseconds{1};
options.retry_timeout = std::chrono::milliseconds{25}; options.retry_timeout = std::chrono::milliseconds{25};
auto backup_future = boost::asio::co_spawn(io, connection.async_backup_to_file(backup_path, options), boost::asio::use_future); auto backup_future = boost::asio::co_spawn(io, connection.async_backup_to_file(backup_path, options), boost::asio::use_future);
auto finalize_future = boost::asio::co_spawn(io, statement->async_finalize(), boost::asio::use_future); auto finalize_future = boost::asio::co_spawn(io, statement->async_finalize(), boost::asio::use_future);
io.run(); io.run();
io.restart(); io.restart();
auto backup = backup_future.get(); auto backup = backup_future.get();
auto finalized_while_busy = finalize_future.get(); auto finalized_while_busy = finalize_future.get();
BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_timeout)); BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_timeout));
BOOST_TEST(static_cast<std::bool_t>(sqlite_exec_ok(blocker, "ROLLBACK"))); BOOST_TEST(static_cast<std::bool_t>(sqlite_exec_ok(blocker, "ROLLBACK")));
sqlite3_close(blocker); sqlite3_close(blocker);
if (!finalized_while_busy) { if (!finalized_while_busy) {
@@ -1028,7 +935,6 @@ BOOST_AUTO_TEST_CASE(noto_backup_lock_timeout_and_statement_finalize_retry_work)
BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize()))); BOOST_TEST(static_cast<std::bool_t>(run(io, statement->async_finalize())));
} }
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
std::filesystem::remove(source_path); std::filesystem::remove(source_path);
std::filesystem::remove(backup_path); std::filesystem::remove(backup_path);
} }
@@ -1044,7 +950,6 @@ BOOST_AUTO_TEST_CASE(noto_export_preserves_nul_text_and_defers_triggers_until_af
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO audit(count) VALUES(0)"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO audit(count) VALUES(0)")));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(value TEXT NOT NULL)")));
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN UPDATE audit SET count = count + 1; END"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN UPDATE audit SET count = count + 1; END")));
auto insert = run(io, connection.async_prepare("INSERT INTO items(value) VALUES(?1)")); auto insert = run(io, connection.async_prepare("INSERT INTO items(value) VALUES(?1)"));
BOOST_REQUIRE(insert); BOOST_REQUIRE(insert);
auto embedded = std::string{ auto embedded = std::string{
@@ -1055,20 +960,17 @@ BOOST_AUTO_TEST_CASE(noto_export_preserves_nul_text_and_defers_triggers_until_af
noto::sqlite_value{embedded}, noto::sqlite_value{embedded},
}))); })));
BOOST_TEST(static_cast<std::bool_t>(run(io, insert->async_finalize()))); BOOST_TEST(static_cast<std::bool_t>(run(io, insert->async_finalize())));
auto exported = run(io, connection.async_export_sql()); auto exported = run(io, connection.async_export_sql());
BOOST_REQUIRE(exported); BOOST_REQUIRE(exported);
BOOST_TEST(exported->find("CAST(X'610062' AS TEXT)") != std::string::npos); BOOST_TEST(exported->find("CAST(X'610062' AS TEXT)") != std::string::npos);
BOOST_TEST(exported->find("INSERT INTO \"items\"") < exported->find("CREATE TRIGGER")); BOOST_TEST(exported->find("INSERT INTO \"items\"") < exported->find("CREATE TRIGGER"));
auto *restored = static_cast<sqlite3 *>(nullptr);
auto* restored = static_cast<sqlite3*>(nullptr);
BOOST_REQUIRE(sqlite3_open(":memory:", &restored) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open(":memory:", &restored) == SQLITE_OK);
BOOST_REQUIRE(sqlite_exec_ok(restored, *exported)); BOOST_REQUIRE(sqlite_exec_ok(restored, *exported));
auto *statement = static_cast<sqlite3_stmt *>(nullptr);
auto* statement = static_cast<sqlite3_stmt*>(nullptr);
BOOST_REQUIRE(sqlite3_prepare_v2(restored, "SELECT value FROM items", -1, &statement, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_prepare_v2(restored, "SELECT value FROM items", -1, &statement, nullptr) == SQLITE_OK);
BOOST_REQUIRE(sqlite3_step(statement) == SQLITE_ROW); BOOST_REQUIRE(sqlite3_step(statement) == SQLITE_ROW);
auto const* text = reinterpret_cast<std::char_t const*>(sqlite3_column_text(statement, 0)); auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement, 0));
auto bytes = sqlite3_column_bytes(statement, 0); auto bytes = sqlite3_column_bytes(statement, 0);
auto restored_text = std::string{ auto restored_text = std::string{
text, text,
@@ -1076,7 +978,6 @@ BOOST_AUTO_TEST_CASE(noto_export_preserves_nul_text_and_defers_triggers_until_af
}; };
sqlite3_finalize(statement); sqlite3_finalize(statement);
BOOST_TEST(restored_text == embedded); BOOST_TEST(restored_text == embedded);
statement = nullptr; statement = nullptr;
BOOST_REQUIRE(sqlite3_prepare_v2(restored, "SELECT count FROM audit", -1, &statement, nullptr) == SQLITE_OK); BOOST_REQUIRE(sqlite3_prepare_v2(restored, "SELECT count FROM audit", -1, &statement, nullptr) == SQLITE_OK);
BOOST_REQUIRE(sqlite3_step(statement) == SQLITE_ROW); BOOST_REQUIRE(sqlite3_step(statement) == SQLITE_ROW);
@@ -1095,14 +996,12 @@ BOOST_AUTO_TEST_CASE(noto_export_preserves_autoincrement_sequence_and_generated_
BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL, value_size INTEGER GENERATED ALWAYS AS (length(value)) STORED)"))); BOOST_REQUIRE(run(io, connection.async_execute("CREATE TABLE items(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL, value_size INTEGER GENERATED ALWAYS AS (length(value)) STORED)")));
BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')"))); BOOST_REQUIRE(run(io, connection.async_execute("INSERT INTO items(value) VALUES('one'), ('two')")));
BOOST_REQUIRE(run(io, connection.async_execute("DELETE FROM items WHERE value = 'two'"))); BOOST_REQUIRE(run(io, connection.async_execute("DELETE FROM items WHERE value = 'two'")));
auto exported = run(io, connection.async_export_sql()); auto exported = run(io, connection.async_export_sql());
BOOST_REQUIRE(exported); BOOST_REQUIRE(exported);
BOOST_TEST(exported->find("sqlite_sequence") != std::string::npos); BOOST_TEST(exported->find("sqlite_sequence") != std::string::npos);
BOOST_TEST(exported->find("value_size") != std::string::npos); BOOST_TEST(exported->find("value_size") != std::string::npos);
BOOST_TEST(exported->find("INSERT INTO \"items\"(\"id\", \"value\")") != std::string::npos); BOOST_TEST(exported->find("INSERT INTO \"items\"(\"id\", \"value\")") != std::string::npos);
auto *restored = static_cast<sqlite3 *>(nullptr);
auto* restored = static_cast<sqlite3*>(nullptr);
BOOST_REQUIRE(sqlite3_open(":memory:", &restored) == SQLITE_OK); BOOST_REQUIRE(sqlite3_open(":memory:", &restored) == SQLITE_OK);
BOOST_REQUIRE(sqlite_exec_ok(restored, *exported)); BOOST_REQUIRE(sqlite_exec_ok(restored, *exported));
BOOST_REQUIRE(sqlite_exec_ok(restored, "INSERT INTO items(value) VALUES('three')")); BOOST_REQUIRE(sqlite_exec_ok(restored, "INSERT INTO items(value) VALUES('three')"));
@@ -1122,7 +1021,6 @@ BOOST_AUTO_TEST_CASE(noto_export_rejects_virtual_tables) {
if (!created) { if (!created) {
return; return;
} }
auto exported = run(io, connection.async_export_sql()); auto exported = run(io, connection.async_export_sql());
BOOST_REQUIRE(!exported); BOOST_REQUIRE(!exported);
BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
@@ -1135,7 +1033,6 @@ BOOST_AUTO_TEST_CASE(noto_file_helpers_reject_null_byte_paths) {
noto::memory_connection_config("path_validation"), noto::memory_connection_config("path_validation"),
}; };
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_connect())));
auto bad_path = std::string{ auto bad_path = std::string{
"bad\0path", "bad\0path",
8, 8,
@@ -1144,6 +1041,5 @@ BOOST_AUTO_TEST_CASE(noto_file_helpers_reject_null_byte_paths) {
BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(backup.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
auto exported = run(io, connection.async_export_sql_to_file(bad_path)); auto exported = run(io, connection.async_export_sql_to_file(bad_path));
BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument)); BOOST_TEST(static_cast<std::int32_t>(exported.error().code()) == static_cast<std::int32_t>(noto::detail::error_invalid_argument));
BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close()))); BOOST_TEST(static_cast<std::bool_t>(run(io, connection.async_close())));
} }