@@ -14,6 +14,13 @@ find_package(modulus CONFIG REQUIRED COMPONENTS boost_json sqlite)
|
||||
|
||||
add_library(noto
|
||||
src/detail/core.cpp
|
||||
src/detail/sqlite_backup.cpp
|
||||
src/detail/sqlite_export.cpp
|
||||
src/detail/sqlite_files.cpp
|
||||
src/detail/sqlite_query.cpp
|
||||
src/detail/sqlite_support.cpp
|
||||
src/detail/statement_core.cpp
|
||||
src/detail/transaction_core.cpp
|
||||
src/detail/errors.cpp
|
||||
src/detail/sql.cpp
|
||||
src/connection.cpp
|
||||
|
||||
@@ -4,6 +4,8 @@ Noto is a C++23 embedded SQLite database management library. It provides connect
|
||||
|
||||
Noto uses Modulus for shared build tooling, Boost.JSON, and SQLite integration. Product-specific schemas and migrations belong in consuming applications.
|
||||
|
||||
Database coroutines serialize operations on the connection strand and execute SQLite calls on the supplied executor. They do not move blocking work to a background thread. Applications that need responsive control or network loops should supply a dedicated database executor. Private implementations separate connection/statement/transaction ownership, query execution, filesystem protection, backup/restore, and SQL export; the public API and transaction semantics are unchanged.
|
||||
|
||||
## 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.
|
||||
|
||||
+4
-1856
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,309 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <noto/errors.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
auto validate_backup_options(noto::backup_options const &options) -> std::expected<void, modulus::error> {
|
||||
if (options.pages_per_step <= 0) {
|
||||
return invalid("backup pages per step must be positive");
|
||||
}
|
||||
auto source = validate_database_name(options.source_name, "backup source");
|
||||
if (!source) {
|
||||
return source;
|
||||
}
|
||||
auto destination = validate_database_name(options.destination_name, "backup destination");
|
||||
if (!destination) {
|
||||
return destination;
|
||||
}
|
||||
auto delay_status = validate_sqlite_milliseconds(options.retry_delay, "backup retry delay");
|
||||
if (!delay_status) {
|
||||
return delay_status;
|
||||
}
|
||||
if (options.retry_timeout) {
|
||||
auto timeout_status = validate_sqlite_milliseconds(*options.retry_timeout, "backup retry timeout");
|
||||
if (!timeout_status) {
|
||||
return timeout_status;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto validate_export_options(noto::export_options const &options) -> std::expected<void, modulus::error> {
|
||||
if (options.rows_per_yield == 0) {
|
||||
return invalid("SQL export rows per yield must be positive");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 timer = boost::asio::steady_timer{core.strand};
|
||||
while (true) {
|
||||
auto code = sqlite3_backup_step(backup, options.pages_per_step);
|
||||
if (code == SQLITE_DONE || (code != SQLITE_OK && code != SQLITE_BUSY && code != SQLITE_LOCKED)) {
|
||||
co_return code;
|
||||
}
|
||||
if (code == SQLITE_OK) {
|
||||
co_await boost::asio::post(core.strand, boost::asio::use_awaitable);
|
||||
continue;
|
||||
}
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
co_return std::unexpected(timeout(std::string{operation} + " timed out while waiting for SQLite locks").error());
|
||||
}
|
||||
auto delay = options.retry_delay;
|
||||
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
|
||||
if (remaining < delay) {
|
||||
delay = remaining;
|
||||
}
|
||||
if (delay.count() <= 0) {
|
||||
co_return std::unexpected(timeout(std::string{operation} + " timed out while waiting for SQLite locks").error());
|
||||
}
|
||||
timer.expires_after(delay);
|
||||
co_await timer.async_wait(boost::asio::use_awaitable);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (close_code == SQLITE_OK) {
|
||||
return status;
|
||||
}
|
||||
if (status) {
|
||||
status = noto::result_from(close_code, handle, operation);
|
||||
}
|
||||
sqlite3_close_v2(handle);
|
||||
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 *source = static_cast<sqlite3 *>(nullptr);
|
||||
auto code = sqlite3_open_v2(path.c_str(), &source, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr);
|
||||
if (code != SQLITE_OK) {
|
||||
auto status = noto::result_from(code, source, "open restore source");
|
||||
if (source != nullptr) {
|
||||
sqlite3_close(source);
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
auto status = configure_busy_timeout(source, std::chrono::milliseconds{0}, "disable source busy timeout for restore");
|
||||
if (!status) {
|
||||
sqlite3_close(source);
|
||||
co_return status;
|
||||
}
|
||||
auto *destination = static_cast<sqlite3 *>(nullptr);
|
||||
code = sqlite3_open_v2(temporary.c_str(), &destination, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr);
|
||||
if (code != SQLITE_OK) {
|
||||
status = noto::result_from(code, destination, "open restore temporary destination");
|
||||
if (destination != nullptr) {
|
||||
sqlite3_close(destination);
|
||||
}
|
||||
sqlite3_close(source);
|
||||
co_return status;
|
||||
}
|
||||
status = configure_busy_timeout(destination, std::chrono::milliseconds{0}, "disable temporary busy timeout for restore");
|
||||
if (!status) {
|
||||
sqlite3_close(destination);
|
||||
sqlite3_close(source);
|
||||
co_return status;
|
||||
}
|
||||
auto *backup = sqlite3_backup_init(destination, "main", source, options.source_name.c_str());
|
||||
if (backup == nullptr) {
|
||||
status = noto::result_from(sqlite3_errcode(destination), destination, "initialize restore");
|
||||
status = close_sqlite_handle(destination, "close restore temporary destination", status);
|
||||
status = close_sqlite_handle(source, "close restore source", status);
|
||||
co_return status;
|
||||
}
|
||||
auto stepped = co_await async_step_backup(core, backup, options, "restore database");
|
||||
auto finish_code = sqlite3_backup_finish(backup);
|
||||
status = stepped ? noto::result_from(*stepped == SQLITE_DONE ? finish_code : *stepped, destination, "restore database") : noto::detail::fail_void(stepped.error());
|
||||
status = close_sqlite_handle(destination, "close restore temporary destination", status);
|
||||
status = close_sqlite_handle(source, "close restore source", status);
|
||||
co_return status;
|
||||
}
|
||||
|
||||
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");
|
||||
if (!checkpoint) {
|
||||
return checkpoint;
|
||||
}
|
||||
auto *closing = std::exchange(core.database, nullptr);
|
||||
auto code = sqlite3_close(closing);
|
||||
if (code == SQLITE_OK) {
|
||||
return {};
|
||||
}
|
||||
core.database = closing;
|
||||
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 *reopened = static_cast<sqlite3 *>(nullptr);
|
||||
auto status = open_configured_database(core.config, core.busy_timeout, &reopened);
|
||||
if (!status) {
|
||||
core.set_state(noto::connection_state::FAILED);
|
||||
return status;
|
||||
}
|
||||
core.database = reopened;
|
||||
core.set_state(noto::connection_state::CONNECTED);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto restore_atomic_failure(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
|
||||
if (core.database != nullptr) {
|
||||
core.set_state(noto::connection_state::CONNECTED);
|
||||
return status;
|
||||
}
|
||||
auto reopened = reopen_database_after_restore(core);
|
||||
return reopened ? status : reopened;
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_backup_to_file(std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "backup database");
|
||||
if (!ready) {
|
||||
co_return ready;
|
||||
}
|
||||
auto path_status = validate_path(path, "backup destination");
|
||||
if (!path_status) {
|
||||
co_return finish_operation(*this, path_status);
|
||||
}
|
||||
auto options_status = validate_backup_options(options);
|
||||
if (!options_status) {
|
||||
co_return finish_operation(*this, options_status);
|
||||
}
|
||||
auto destination_status = validate_output_not_live_database(database, path, "backup destination");
|
||||
if (!destination_status) {
|
||||
co_return finish_operation(*this, destination_status);
|
||||
}
|
||||
auto temporary = make_temporary_path(path, "create backup temporary file");
|
||||
if (!temporary) {
|
||||
co_return finish_operation(*this, noto::detail::fail_void(temporary.error()));
|
||||
}
|
||||
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);
|
||||
if (code != SQLITE_OK) {
|
||||
auto status = noto::result_from(code, destination, "open backup destination");
|
||||
if (destination != nullptr) {
|
||||
sqlite3_close(destination);
|
||||
}
|
||||
remove_file_noexcept(*temporary);
|
||||
co_return finish_operation(*this, status);
|
||||
}
|
||||
auto timeout_disabled = configure_busy_timeout(database, std::chrono::milliseconds{0}, "disable source busy timeout for backup");
|
||||
if (!timeout_disabled) {
|
||||
sqlite3_close(destination);
|
||||
remove_file_noexcept(*temporary);
|
||||
co_return finish_operation(*this, timeout_disabled);
|
||||
}
|
||||
auto restore_source_timeout = [this](std::expected<void, modulus::error> status) {
|
||||
auto restored = configure_busy_timeout(database, busy_timeout, "restore source busy timeout after backup");
|
||||
return status ? restored : status;
|
||||
};
|
||||
auto *backup = sqlite3_backup_init(destination, options.destination_name.c_str(), database, options.source_name.c_str());
|
||||
if (backup == nullptr) {
|
||||
auto status = noto::result_from(sqlite3_errcode(destination), destination, "initialize backup");
|
||||
sqlite3_close(destination);
|
||||
remove_file_noexcept(*temporary);
|
||||
co_return finish_operation(*this, restore_source_timeout(status));
|
||||
}
|
||||
auto stepped = co_await async_step_backup(*this, backup, options, "backup database");
|
||||
auto finish_code = sqlite3_backup_finish(backup);
|
||||
auto status = stepped ? noto::result_from(*stepped == SQLITE_DONE ? finish_code : *stepped, destination, "backup database") : noto::detail::fail_void(stepped.error());
|
||||
status = restore_source_timeout(status);
|
||||
auto close_code = sqlite3_close(destination);
|
||||
if (close_code != SQLITE_OK) {
|
||||
if (status) {
|
||||
status = noto::result_from(close_code, destination, "close backup destination");
|
||||
}
|
||||
sqlite3_close_v2(destination);
|
||||
}
|
||||
if (status) {
|
||||
status = replace_file_atomically(*temporary, path, "replace backup destination");
|
||||
}
|
||||
if (!status) {
|
||||
remove_file_noexcept(*temporary);
|
||||
}
|
||||
co_return finish_operation(*this, status);
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_restore_from_file(std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "restore database");
|
||||
if (!ready) {
|
||||
co_return ready;
|
||||
}
|
||||
auto path_status = validate_path(path, "restore source");
|
||||
if (!path_status) {
|
||||
co_return finish_operation(*this, path_status);
|
||||
}
|
||||
auto options_status = validate_backup_options(options);
|
||||
if (!options_status) {
|
||||
co_return finish_operation(*this, options_status);
|
||||
}
|
||||
if (config.mode == noto::open_mode::MEMORY || config.uri || config.filename == ":memory:") {
|
||||
co_return finish_operation(*this, invalid("atomic restore requires a file-backed database"));
|
||||
}
|
||||
if (config.mode == noto::open_mode::READ_ONLY) {
|
||||
co_return finish_operation(*this, invalid("atomic restore requires a writable database"));
|
||||
}
|
||||
if (options.destination_name != "main") {
|
||||
co_return finish_operation(*this, invalid("atomic restore only supports the main destination database"));
|
||||
}
|
||||
if (transaction_active) {
|
||||
co_return finish_operation(*this, unavailable("database connection cannot restore while a transaction is active"));
|
||||
}
|
||||
if (live_statements.load(std::memory_order_acquire) != 0) {
|
||||
co_return finish_operation(*this, unavailable("database connection cannot restore while prepared statements are active"));
|
||||
}
|
||||
auto target_status = validate_path(config.filename, "restore destination");
|
||||
if (!target_status) {
|
||||
co_return finish_operation(*this, target_status);
|
||||
}
|
||||
auto temporary = make_temporary_path(config.filename, "create restore temporary file");
|
||||
if (!temporary) {
|
||||
co_return finish_operation(*this, noto::detail::fail_void(temporary.error()));
|
||||
}
|
||||
auto status = co_await async_copy_restore_source_to_file(*this, path, *temporary, options);
|
||||
if (status) {
|
||||
set_state(noto::connection_state::CLOSING);
|
||||
status = close_database_for_atomic_restore(*this);
|
||||
if (!status) {
|
||||
status = restore_atomic_failure(*this, status);
|
||||
}
|
||||
}
|
||||
if (status) {
|
||||
status = remove_database_sidecars(config.filename, "remove stale database sidecar files before restore");
|
||||
if (!status) {
|
||||
status = restore_atomic_failure(*this, status);
|
||||
}
|
||||
}
|
||||
if (status) {
|
||||
status = replace_file_atomically(*temporary, config.filename, "replace restored database");
|
||||
if (!status) {
|
||||
status = restore_atomic_failure(*this, status);
|
||||
}
|
||||
}
|
||||
if (status) {
|
||||
status = reopen_database_after_restore(*this);
|
||||
}
|
||||
if (!status) {
|
||||
remove_database_files_noexcept(*temporary);
|
||||
}
|
||||
co_return finish_operation(*this, status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <noto/errors.hpp>
|
||||
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
auto hex_literal(std::byte const *data, std::size_t size) -> std::string {
|
||||
auto sql = std::ostringstream{};
|
||||
sql << "X'";
|
||||
for (auto index = std::size_t{}; index < size; ++index) {
|
||||
sql << std::hex << std::setw(2) << std::setfill('0') << static_cast<std::int32_t>(std::to_integer<std::uchar_t>(data[index]));
|
||||
}
|
||||
sql << "'";
|
||||
return sql.str();
|
||||
}
|
||||
|
||||
auto text_hex_literal(std::string const &value) -> std::string {
|
||||
auto const *bytes = reinterpret_cast<std::byte const *>(value.data());
|
||||
return "CAST(" + hex_literal(bytes, value.size()) + " AS TEXT)";
|
||||
}
|
||||
|
||||
auto literal_sql(noto::sqlite_value const &value) -> std::expected<std::string, modulus::error> {
|
||||
return std::visit([](auto const ¤t) -> std::expected<std::string, modulus::error> {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
|
||||
return "NULL";
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
|
||||
return std::to_string(current);
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::float64_t>) {
|
||||
if (!std::isfinite(current)) {
|
||||
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, "exported real value must be finite");
|
||||
}
|
||||
auto sql = std::ostringstream{};
|
||||
sql << std::setprecision(17) << current;
|
||||
return sql.str();
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::string>) {
|
||||
if (contains_null(current)) {
|
||||
return text_hex_literal(current);
|
||||
}
|
||||
auto literal = noto::detail::quote_sql_literal(current, "export value");
|
||||
if (!literal) {
|
||||
return std::unexpected(literal.error());
|
||||
}
|
||||
return *literal;
|
||||
} else {
|
||||
return hex_literal(current.data(), current.size());
|
||||
}
|
||||
},
|
||||
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> {
|
||||
if (text.size() > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
|
||||
return resource_exhausted("SQL export chunk is too large");
|
||||
}
|
||||
if (options.max_output_bytes && (text.size() > *options.max_output_bytes || written > *options.max_output_bytes - text.size())) {
|
||||
return resource_exhausted("SQL export exceeded configured output byte limit");
|
||||
}
|
||||
output.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||
if (!output) {
|
||||
return unavailable("failed to write SQL export output");
|
||||
}
|
||||
if (text.size() > std::numeric_limits<std::size_t>::max() - written) {
|
||||
written = std::numeric_limits<std::size_t>::max();
|
||||
} else {
|
||||
written += text.size();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional<std::string> {
|
||||
if (sqlite3_column_type(statement, index) != SQLITE_TEXT) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement, index));
|
||||
auto bytes = sqlite3_column_bytes(statement, index);
|
||||
return text == nullptr ? std::string{} : std::string{
|
||||
text,
|
||||
static_cast<std::size_t>(bytes),
|
||||
};
|
||||
}
|
||||
|
||||
auto identifier_character(std::char_t character) -> std::bool_t {
|
||||
auto byte = static_cast<std::uchar_t>(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 current = position;
|
||||
while (current < sql.size() && std::isspace(static_cast<std::uchar_t>(sql[current]))) {
|
||||
++current;
|
||||
}
|
||||
if (keyword.size() > sql.size() - current) {
|
||||
return false;
|
||||
}
|
||||
for (auto index = std::size_t{}; index < keyword.size(); ++index) {
|
||||
auto left = static_cast<std::uchar_t>(sql[current + index]);
|
||||
auto right = static_cast<std::uchar_t>(keyword[index]);
|
||||
if (std::tolower(left) != std::tolower(right)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto end = current + keyword.size();
|
||||
if (end < sql.size() && identifier_character(sql[end])) {
|
||||
return false;
|
||||
}
|
||||
position = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto create_virtual_table_sql(std::string_view sql) -> std::bool_t {
|
||||
auto position = std::size_t{};
|
||||
if (!consume_keyword(sql, position, "CREATE")) {
|
||||
return false;
|
||||
}
|
||||
auto after_create = position;
|
||||
if (!consume_keyword(sql, position, "TEMP") && !consume_keyword(sql, position, "TEMPORARY")) {
|
||||
position = after_create;
|
||||
}
|
||||
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 *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");
|
||||
if (!prepared) {
|
||||
return failure_from<std::bool_t>(prepared);
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto code = sqlite3_bind_text(statement.get(), 1, table_name.data(), static_cast<std::int32_t>(table_name.size()), SQLITE_TRANSIENT);
|
||||
auto status = noto::result_from(code, database, "bind table existence check");
|
||||
if (!status) {
|
||||
return failure_from<std::bool_t>(status);
|
||||
}
|
||||
code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_ROW) {
|
||||
return true;
|
||||
}
|
||||
if (code == SQLITE_DONE) {
|
||||
return false;
|
||||
}
|
||||
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 quoted_table = noto::quote_identifier(table_name);
|
||||
if (!quoted_table) {
|
||||
return failure_from<std::vector<export_column>>(quoted_table.error());
|
||||
}
|
||||
auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
|
||||
auto prepared = prepare_statement(database, "PRAGMA table_xinfo(" + *quoted_table + ")", &raw_statement, "prepare export columns");
|
||||
if (!prepared) {
|
||||
return failure_from<std::vector<export_column>>(prepared);
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
if (sqlite3_column_count(statement.get()) < 7) {
|
||||
return noto::detail::fail<std::vector<export_column>>(noto::detail::error_unavailable, "SQLite table_xinfo did not return hidden column metadata");
|
||||
}
|
||||
auto columns = std::vector<export_column>{};
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_DONE) {
|
||||
return columns;
|
||||
}
|
||||
if (code != SQLITE_ROW) {
|
||||
return failure_from<std::vector<export_column>>(noto::result_from(code, database, "collect export columns"));
|
||||
}
|
||||
auto hidden = sqlite3_column_int(statement.get(), 6);
|
||||
if (hidden != 0) {
|
||||
continue;
|
||||
}
|
||||
auto name = column_text(statement.get(), 1);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
auto quoted_name = noto::quote_identifier(*name);
|
||||
if (!quoted_name) {
|
||||
return failure_from<std::vector<export_column>>(quoted_name.error());
|
||||
}
|
||||
columns.push_back(export_column{
|
||||
.quoted_name = std::move(*quoted_name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auto collect_export_schema(sqlite3 *database) -> std::expected<export_schema, modulus::error> {
|
||||
auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
|
||||
auto prepared = prepare_statement(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",
|
||||
&raw_statement, "prepare export schema");
|
||||
if (!prepared) {
|
||||
return failure_from<export_schema>(prepared);
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto schema = export_schema{};
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_ROW) {
|
||||
auto type = column_text(statement.get(), 0);
|
||||
auto name = column_text(statement.get(), 1);
|
||||
auto sql = column_text(statement.get(), 2);
|
||||
if (!type || !name || !sql) {
|
||||
continue;
|
||||
}
|
||||
if (*type == "table") {
|
||||
if (create_virtual_table_sql(*sql)) {
|
||||
return noto::detail::fail<export_schema>(noto::detail::error_invalid_argument, "SQL export does not support virtual tables");
|
||||
}
|
||||
auto quoted_name = noto::quote_identifier(*name);
|
||||
if (!quoted_name) {
|
||||
return failure_from<export_schema>(quoted_name.error());
|
||||
}
|
||||
auto columns = exportable_columns(database, *name);
|
||||
if (!columns) {
|
||||
return failure_from<export_schema>(columns.error());
|
||||
}
|
||||
schema.table_sql.push_back(*sql);
|
||||
schema.tables.push_back(export_table{
|
||||
.quoted_name = std::move(*quoted_name),
|
||||
.columns = std::move(*columns),
|
||||
});
|
||||
} else if (*type == "index" || *type == "view" || *type == "trigger") {
|
||||
schema.post_data_sql.push_back(*sql);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code == SQLITE_DONE) {
|
||||
auto sequence_exists = table_exists(database, "sqlite_sequence");
|
||||
if (!sequence_exists) {
|
||||
return failure_from<export_schema>(sequence_exists.error());
|
||||
}
|
||||
schema.has_sqlite_sequence = *sequence_exists;
|
||||
return schema;
|
||||
}
|
||||
return failure_from<export_schema>(noto::result_from(code, database, "collect export schema"));
|
||||
}
|
||||
}
|
||||
|
||||
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 select_sql = std::string{};
|
||||
if (table.columns.empty()) {
|
||||
select_sql = "SELECT 1 FROM " + table.quoted_name;
|
||||
} else {
|
||||
select_sql = "SELECT ";
|
||||
for (auto index = std::size_t{}; index < table.columns.size(); ++index) {
|
||||
if (index != 0) {
|
||||
select_sql += ", ";
|
||||
}
|
||||
select_sql += table.columns[index].quoted_name;
|
||||
}
|
||||
select_sql += " FROM ";
|
||||
select_sql += table.quoted_name;
|
||||
}
|
||||
auto prepared = prepare_statement(core.database, select_sql, &raw_statement, "prepare export rows");
|
||||
if (!prepared) {
|
||||
co_return prepared;
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto rows_since_yield = std::size_t{};
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_DONE) {
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
if (code != SQLITE_ROW) {
|
||||
co_return noto::result_from(code, core.database, "export table rows");
|
||||
}
|
||||
auto row_sql = std::ostringstream{};
|
||||
if (table.columns.empty()) {
|
||||
row_sql << "INSERT INTO " << table.quoted_name << " DEFAULT VALUES;\n";
|
||||
} else {
|
||||
row_sql << "INSERT INTO " << table.quoted_name << "(";
|
||||
for (auto index = std::size_t{}; index < table.columns.size(); ++index) {
|
||||
if (index != 0) {
|
||||
row_sql << ", ";
|
||||
}
|
||||
row_sql << table.columns[index].quoted_name;
|
||||
}
|
||||
row_sql << ") VALUES(";
|
||||
for (auto index = std::size_t{}; index < table.columns.size(); ++index) {
|
||||
if (index != 0) {
|
||||
row_sql << ", ";
|
||||
}
|
||||
auto literal = literal_sql(column_value(statement.get(), static_cast<std::int32_t>(index)));
|
||||
if (!literal) {
|
||||
co_return std::unexpected(literal.error());
|
||||
}
|
||||
row_sql << *literal;
|
||||
}
|
||||
row_sql << ");\n";
|
||||
}
|
||||
auto status = write(row_sql.str());
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
++rows_since_yield;
|
||||
if (rows_since_yield >= options.rows_per_yield) {
|
||||
rows_since_yield = 0;
|
||||
co_await boost::asio::post(core.strand, boost::asio::use_awaitable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
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");
|
||||
if (!prepared) {
|
||||
co_return prepared;
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto rows_since_yield = std::size_t{};
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_DONE) {
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
if (code != SQLITE_ROW) {
|
||||
co_return noto::result_from(code, core.database, "export sqlite_sequence");
|
||||
}
|
||||
auto name = literal_sql(column_value(statement.get(), 0));
|
||||
if (!name) {
|
||||
co_return std::unexpected(name.error());
|
||||
}
|
||||
auto sequence = literal_sql(column_value(statement.get(), 1));
|
||||
if (!sequence) {
|
||||
co_return std::unexpected(sequence.error());
|
||||
}
|
||||
auto row_sql = std::ostringstream{};
|
||||
row_sql << "INSERT INTO \"sqlite_sequence\"(\"name\", \"seq\") VALUES(" << *name << ", " << *sequence << ");\n";
|
||||
status = write(row_sql.str());
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
++rows_since_yield;
|
||||
if (rows_since_yield >= options.rows_per_yield) {
|
||||
rows_since_yield = 0;
|
||||
co_await boost::asio::post(core.strand, boost::asio::use_awaitable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (!schema) {
|
||||
co_return std::unexpected(schema.error());
|
||||
}
|
||||
auto status = write("PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n");
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
for (auto const &sql : schema->table_sql) {
|
||||
status = write(sql);
|
||||
if (status) {
|
||||
status = write(";\n");
|
||||
}
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
}
|
||||
for (auto const &table : schema->tables) {
|
||||
status = co_await export_table_rows(core, table, write, options);
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
}
|
||||
if (schema->has_sqlite_sequence) {
|
||||
status = co_await export_sqlite_sequence(core, write, options);
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
}
|
||||
for (auto const &sql : schema->post_data_sql) {
|
||||
status = write(sql);
|
||||
if (status) {
|
||||
status = write(";\n");
|
||||
}
|
||||
if (!status) {
|
||||
co_return status;
|
||||
}
|
||||
}
|
||||
co_return write("COMMIT;\nPRAGMA foreign_keys=ON;\n");
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_export_sql(noto::export_options options) -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "export database");
|
||||
if (!ready) {
|
||||
co_return std::unexpected(ready.error());
|
||||
}
|
||||
auto options_status = validate_export_options(options);
|
||||
if (!options_status) {
|
||||
options_status = finish_operation(*this, options_status);
|
||||
co_return std::unexpected(options_status.error());
|
||||
}
|
||||
auto output = std::ostringstream{};
|
||||
auto written = std::size_t{};
|
||||
auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { return write_limited(output, text, options, written); }, options);
|
||||
if (!status) {
|
||||
status = finish_operation(*this, status);
|
||||
co_return std::unexpected(status.error());
|
||||
}
|
||||
static_cast<void>(finish_operation(*this, {}));
|
||||
co_return output.str();
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_export_sql_to_file(std::string path, noto::export_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto path_status = validate_path(path, "export");
|
||||
if (!path_status) {
|
||||
co_return path_status;
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "export database");
|
||||
if (!ready) {
|
||||
co_return ready;
|
||||
}
|
||||
auto options_status = validate_export_options(options);
|
||||
if (!options_status) {
|
||||
co_return finish_operation(*this, options_status);
|
||||
}
|
||||
auto destination_status = validate_output_not_live_database(database, path, "export destination");
|
||||
if (!destination_status) {
|
||||
co_return finish_operation(*this, destination_status);
|
||||
}
|
||||
auto temporary = make_temporary_path(path, "create export temporary file");
|
||||
if (!temporary) {
|
||||
co_return finish_operation(*this, noto::detail::fail_void(temporary.error()));
|
||||
}
|
||||
auto output = std::ofstream{
|
||||
*temporary,
|
||||
std::ios::binary | std::ios::trunc,
|
||||
};
|
||||
if (!output) {
|
||||
remove_file_noexcept(*temporary);
|
||||
co_return finish_operation(*this, unavailable("failed to open export file"));
|
||||
}
|
||||
auto written = std::size_t{};
|
||||
auto status = co_await write_export(*this, [&output, &options, &written](std::string_view text) { return write_limited(output, text, options, written); }, options);
|
||||
if (status) {
|
||||
output.flush();
|
||||
if (!output) {
|
||||
status = unavailable("failed to write export file");
|
||||
}
|
||||
}
|
||||
output.close();
|
||||
if (status && !output) {
|
||||
status = unavailable("failed to close export file");
|
||||
}
|
||||
if (status) {
|
||||
status = replace_file_atomically(*temporary, path, "replace export file");
|
||||
}
|
||||
if (!status) {
|
||||
remove_file_noexcept(*temporary);
|
||||
}
|
||||
co_return finish_operation(*this, status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <noto/errors.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
constexpr auto sqlite_sidecar_suffixes = std::array<std::string_view, 3>{"-wal", "-shm", "-journal"};
|
||||
|
||||
auto errno_status(std::string operation, std::int32_t code) -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + std::error_code{
|
||||
code,
|
||||
std::generic_category(),
|
||||
}
|
||||
.message());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
auto close_fd(std::int32_t fd, std::string_view operation) -> std::expected<void, modulus::error> {
|
||||
if (::close(fd) == 0) {
|
||||
return {};
|
||||
}
|
||||
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 fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
return errno_status(std::string{operation}, errno);
|
||||
}
|
||||
if (::fsync(fd) != 0) {
|
||||
auto status = errno_status(std::string{operation}, errno);
|
||||
static_cast<void>(::close(fd));
|
||||
return status;
|
||||
}
|
||||
return close_fd(fd, operation);
|
||||
}
|
||||
|
||||
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();
|
||||
if (parent.empty()) {
|
||||
parent = ".";
|
||||
}
|
||||
auto directory = parent.string();
|
||||
auto fd = ::open(directory.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
return errno_status(std::string{operation}, errno);
|
||||
}
|
||||
if (::fsync(fd) != 0) {
|
||||
auto status = errno_status(std::string{operation}, errno);
|
||||
static_cast<void>(::close(fd));
|
||||
return status;
|
||||
}
|
||||
return close_fd(fd, operation);
|
||||
}
|
||||
|
||||
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 parent = target_path.parent_path();
|
||||
if (parent.empty()) {
|
||||
parent = ".";
|
||||
}
|
||||
auto filename = target_path.filename().string();
|
||||
if (filename.empty()) {
|
||||
filename = "noto";
|
||||
}
|
||||
auto pattern = (parent / (filename + ".noto-tmp-XXXXXX")).string();
|
||||
auto buffer = std::vector<std::char_t>{
|
||||
pattern.begin(),
|
||||
pattern.end(),
|
||||
};
|
||||
buffer.push_back('\0');
|
||||
auto fd = ::mkstemp(buffer.data());
|
||||
if (fd < 0) {
|
||||
return std::unexpected(errno_status(std::string{operation}, errno).error());
|
||||
}
|
||||
auto path = std::string{buffer.data()};
|
||||
auto closed = close_fd(fd, operation);
|
||||
if (!closed) {
|
||||
static_cast<void>(::unlink(path.c_str()));
|
||||
return std::unexpected(closed.error());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
auto remove_file_noexcept(std::string const &path) noexcept -> void {
|
||||
if (!path.empty()) {
|
||||
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> {
|
||||
if (::unlink(path.c_str()) == 0 || errno == ENOENT) {
|
||||
return {};
|
||||
}
|
||||
return errno_status(std::string{operation}, errno);
|
||||
}
|
||||
|
||||
auto remove_database_sidecars(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
|
||||
for (auto suffix : sqlite_sidecar_suffixes) {
|
||||
auto status = remove_file_if_exists(path + std::string{suffix}, operation);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto remove_database_files_noexcept(std::string const &path) noexcept -> void {
|
||||
remove_file_noexcept(path);
|
||||
for (auto suffix : sqlite_sidecar_suffixes) {
|
||||
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 status = sync_file_path(temporary, operation);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
if (::rename(temporary.c_str(), destination.c_str()) != 0) {
|
||||
return errno_status(std::string{operation}, errno);
|
||||
}
|
||||
return sync_parent_directory(destination, operation);
|
||||
}
|
||||
|
||||
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 code = sqlite3_prepare_v2(database, sql.c_str(), -1, statement, &tail);
|
||||
auto status = noto::result_from(code, database, operation);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
if (*statement == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (!trailing_sql_is_empty(tail)) {
|
||||
sqlite3_finalize(*statement);
|
||||
*statement = nullptr;
|
||||
return invalid("only one SQL statement may be executed at a time");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto same_identity(file_identity const &left, file_identity const &right) -> std::bool_t {
|
||||
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 error = std::error_code{};
|
||||
auto absolute = std::filesystem::absolute(path, error);
|
||||
if (error) {
|
||||
return std::unexpected(filesystem_status(std::string{operation}, error).error());
|
||||
}
|
||||
auto normalized = std::filesystem::weakly_canonical(absolute, error);
|
||||
if (error) {
|
||||
return std::unexpected(filesystem_status(std::string{operation}, error).error());
|
||||
}
|
||||
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> {
|
||||
struct stat data{};
|
||||
|
||||
auto text = path.string();
|
||||
if (::stat(text.c_str(), &data) == 0) {
|
||||
auto identity = file_identity{};
|
||||
identity.device = data.st_dev;
|
||||
identity.inode = data.st_ino;
|
||||
return std::optional<file_identity>{identity};
|
||||
}
|
||||
if (errno == ENOENT || errno == ENOTDIR) {
|
||||
return std::optional<file_identity>{};
|
||||
}
|
||||
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 normalized = normalized_path(path, operation);
|
||||
if (!normalized) {
|
||||
return std::unexpected(normalized.error());
|
||||
}
|
||||
auto identity = identity_for_path(*normalized, operation);
|
||||
if (!identity) {
|
||||
return std::unexpected(identity.error());
|
||||
}
|
||||
return protected_path{
|
||||
.normalized = std::move(*normalized),
|
||||
.identity = std::move(*identity),
|
||||
};
|
||||
}
|
||||
|
||||
auto same_protected_path(protected_path const &left, protected_path const &right) -> std::bool_t {
|
||||
if (left.normalized == right.normalized) {
|
||||
return true;
|
||||
}
|
||||
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 *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
|
||||
auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list");
|
||||
if (!prepared) {
|
||||
return failure_from<std::vector<std::string>>(prepared);
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto paths = std::vector<std::string>{};
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement.get());
|
||||
if (code == SQLITE_DONE) {
|
||||
return paths;
|
||||
}
|
||||
if (code != SQLITE_ROW) {
|
||||
return failure_from<std::vector<std::string>>(noto::result_from(code, database, "collect open database list"));
|
||||
}
|
||||
if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) {
|
||||
continue;
|
||||
}
|
||||
auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement.get(), 2));
|
||||
auto bytes = sqlite3_column_bytes(statement.get(), 2);
|
||||
auto path = text == nullptr ? std::string{} : std::string{
|
||||
text,
|
||||
static_cast<std::size_t>(bytes),
|
||||
};
|
||||
if (!path.empty()) {
|
||||
paths.push_back(std::move(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
if (!target) {
|
||||
return std::unexpected(target.error());
|
||||
}
|
||||
auto live_paths = live_database_paths(database);
|
||||
if (!live_paths) {
|
||||
return std::unexpected(live_paths.error());
|
||||
}
|
||||
for (auto const &live_path : *live_paths) {
|
||||
auto protected_live = protected_path_for(live_path, "check open database path");
|
||||
if (!protected_live) {
|
||||
return std::unexpected(protected_live.error());
|
||||
}
|
||||
if (same_protected_path(*target, *protected_live)) {
|
||||
return invalid(std::string{label} + " must not target an open database file");
|
||||
}
|
||||
for (auto suffix : sqlite_sidecar_suffixes) {
|
||||
auto protected_sidecar = protected_path_for(live_path + std::string{suffix}, "check open database sidecar path");
|
||||
if (!protected_sidecar) {
|
||||
return std::unexpected(protected_sidecar.error());
|
||||
}
|
||||
if (same_protected_path(*target, *protected_sidecar)) {
|
||||
return invalid(std::string{label} + " must not target an open database sidecar");
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <noto/errors.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
auto validate_database_name(std::string_view name, std::string_view label) -> std::expected<void, modulus::error> {
|
||||
if (name.empty()) {
|
||||
return invalid(std::string{label} + " database name must not be empty");
|
||||
}
|
||||
if (contains_null(name)) {
|
||||
return invalid(std::string{label} + " database name must not contain null bytes");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
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 code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, &error);
|
||||
if (code == SQLITE_OK) {
|
||||
return {};
|
||||
}
|
||||
auto message = std::string{};
|
||||
if (!operation.empty()) {
|
||||
message += operation;
|
||||
message += ": ";
|
||||
}
|
||||
message += error == nullptr ? sqlite3_errmsg(database) : error;
|
||||
sqlite3_free(error);
|
||||
auto mapped = noto::error_from(code, database);
|
||||
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> {
|
||||
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> {
|
||||
return std::visit([](auto const ¤t) -> std::expected<std::string, modulus::error> {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::bool_t>) {
|
||||
return std::string{current ? "1" : "0"};
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
|
||||
return std::to_string(current);
|
||||
} else {
|
||||
return noto::detail::quote_sql_literal(current, "pragma 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> {
|
||||
if (!valid_token(name)) {
|
||||
return invalid("database pragma names must contain only letters, digits, or underscores");
|
||||
}
|
||||
auto rendered = pragma_value_sql(value);
|
||||
if (!rendered) {
|
||||
return std::unexpected(rendered.error());
|
||||
}
|
||||
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> {
|
||||
*output = nullptr;
|
||||
auto *next_database = static_cast<sqlite3 *>(nullptr);
|
||||
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);
|
||||
if (code != SQLITE_OK) {
|
||||
auto status = noto::result_from(code, next_database, "open database");
|
||||
if (next_database != nullptr) {
|
||||
sqlite3_close(next_database);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
sqlite3_extended_result_codes(next_database, 1);
|
||||
auto status = configure_busy_timeout(next_database, busy_timeout, "configure busy timeout");
|
||||
if (status) {
|
||||
status = exec_direct(next_database, std::string{"PRAGMA foreign_keys = "} + (config.foreign_keys ? "1" : "0"), "configure foreign keys");
|
||||
}
|
||||
for (auto const &pragma : config.pragmas) {
|
||||
if (!status) {
|
||||
break;
|
||||
}
|
||||
status = set_pragma(next_database, pragma.name, pragma.value);
|
||||
}
|
||||
if (!status) {
|
||||
sqlite3_close(next_database);
|
||||
return status;
|
||||
}
|
||||
*output = next_database;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto statement_is_open(noto::detail::statement_core const &core) -> std::bool_t {
|
||||
return core.statement != nullptr && !core.finalized.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
auto untrack_statement(noto::detail::statement_core &statement) -> void {
|
||||
if (!statement.counted) {
|
||||
return;
|
||||
}
|
||||
statement.counted = false;
|
||||
if (statement.connection && statement.connection->live_statements.load(std::memory_order_acquire) > 0) {
|
||||
statement.connection->live_statements.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
|
||||
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 ¤t) -> 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>>) {
|
||||
return current.size();
|
||||
} else {
|
||||
return std::size_t{};
|
||||
}
|
||||
},
|
||||
value.value);
|
||||
if (size > sqlite_int_max()) {
|
||||
return invalid("bound SQLite text/blob value is too large");
|
||||
}
|
||||
auto code = std::visit([statement, index](auto const ¤t) -> std::int32_t {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
|
||||
return sqlite3_bind_null(statement, index);
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
|
||||
return sqlite3_bind_int64(statement, index, static_cast<sqlite3_int64>(current));
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::float64_t>) {
|
||||
return sqlite3_bind_double(statement, index, current);
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::string>) {
|
||||
return sqlite3_bind_text(statement, index, current.data(), static_cast<std::int32_t>(current.size()), SQLITE_TRANSIENT);
|
||||
} else {
|
||||
return sqlite3_bind_blob(statement, index, current.data(), static_cast<std::int32_t>(current.size()), SQLITE_TRANSIENT);
|
||||
}
|
||||
},
|
||||
value.value);
|
||||
return noto::result_from(code, database, "bind parameter");
|
||||
}
|
||||
|
||||
auto bind_values(sqlite3_stmt *statement, std::vector<noto::sqlite_value> const ¶ms, sqlite3 *database) -> std::expected<void, modulus::error> {
|
||||
if (params.size() > sqlite_int_max()) {
|
||||
return invalid("prepared statement parameter count is too large");
|
||||
}
|
||||
auto expected = sqlite3_bind_parameter_count(statement);
|
||||
if (expected != static_cast<std::int32_t>(params.size())) {
|
||||
return invalid("prepared statement parameter count does not match bound value count");
|
||||
}
|
||||
for (auto index = std::size_t{}; index < params.size(); ++index) {
|
||||
auto status = bind_value(statement, static_cast<std::int32_t>(index + 1), params[index], database);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto column_value(sqlite3_stmt *statement, std::int32_t index) -> noto::sqlite_value {
|
||||
switch (sqlite3_column_type(statement, index)) {
|
||||
case SQLITE_INTEGER:
|
||||
return noto::sqlite_value{static_cast<std::int64_t>(sqlite3_column_int64(statement, index))};
|
||||
case SQLITE_FLOAT:
|
||||
return noto::sqlite_value{sqlite3_column_double(statement, index)};
|
||||
case SQLITE_TEXT: {
|
||||
auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement, index));
|
||||
auto bytes = sqlite3_column_bytes(statement, index);
|
||||
return noto::sqlite_value{
|
||||
text == nullptr ? std::string{} : std::string{
|
||||
text,
|
||||
static_cast<std::size_t>(bytes),
|
||||
},
|
||||
};
|
||||
}
|
||||
case SQLITE_BLOB: {
|
||||
auto const *data = static_cast<std::byte const *>(sqlite3_column_blob(statement, index));
|
||||
auto bytes = sqlite3_column_bytes(statement, index);
|
||||
auto blob = std::vector<std::byte>{};
|
||||
if (data != nullptr && bytes > 0) {
|
||||
blob.assign(data, data + bytes);
|
||||
}
|
||||
return noto::sqlite_value{std::move(blob)};
|
||||
}
|
||||
case SQLITE_NULL:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
auto current_row(sqlite3_stmt *statement) -> std::vector<noto::sqlite_value> {
|
||||
auto count = sqlite3_column_count(statement);
|
||||
auto row = std::vector<noto::sqlite_value>{};
|
||||
row.reserve(static_cast<std::size_t>(count));
|
||||
for (auto index = 0; index < count; ++index) {
|
||||
row.push_back(column_value(statement, index));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
auto column_names(sqlite3_stmt *statement) -> std::vector<std::string> {
|
||||
auto count = sqlite3_column_count(statement);
|
||||
auto names = std::vector<std::string>{};
|
||||
names.reserve(static_cast<std::size_t>(count));
|
||||
for (auto index = 0; index < count; ++index) {
|
||||
auto const *name = sqlite3_column_name(statement, index);
|
||||
names.push_back(name == nullptr ? std::string{} : std::string{name});
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
auto value_result_bytes(noto::sqlite_value const &value) -> std::size_t {
|
||||
return std::visit([](auto const ¤t) -> std::size_t {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::monostate>) {
|
||||
return {};
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::int64_t>) {
|
||||
return sizeof(std::int64_t);
|
||||
} else if constexpr (std::is_same_v<std::decay_t<decltype(current)>, std::float64_t>) {
|
||||
return sizeof(std::float64_t);
|
||||
} else {
|
||||
return current.size();
|
||||
}
|
||||
},
|
||||
value.value);
|
||||
}
|
||||
|
||||
auto add_result_bytes(std::size_t ¤t, std::size_t addition, noto::execute_options const &options) -> std::expected<void, modulus::error> {
|
||||
if (!options.max_result_bytes) {
|
||||
if (addition > std::numeric_limits<std::size_t>::max() - current) {
|
||||
current = std::numeric_limits<std::size_t>::max();
|
||||
} else {
|
||||
current += addition;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (addition > *options.max_result_bytes || current > *options.max_result_bytes - addition) {
|
||||
return resource_exhausted("query result exceeded configured byte limit");
|
||||
}
|
||||
current += addition;
|
||||
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> {
|
||||
for (auto const &column : columns) {
|
||||
auto status = add_result_bytes(result_bytes, column.size(), options);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
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> {
|
||||
if (options.max_rows && result.rows.size() >= *options.max_rows) {
|
||||
return resource_exhausted("query returned more rows than configured limit");
|
||||
}
|
||||
for (auto const &value : row) {
|
||||
auto status = add_result_bytes(result_bytes, value_result_bytes(value), options);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
result.rows.push_back(std::move(row));
|
||||
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 = noto::result_from(sqlite3_reset(statement), database, "reset statement after execute failure");
|
||||
return reset ? status : reset;
|
||||
}
|
||||
|
||||
auto reset_and_bind(sqlite3_stmt *statement, sqlite3 *database, std::vector<noto::sqlite_value> const ¶ms) -> std::expected<void, modulus::error> {
|
||||
auto code = sqlite3_reset(statement);
|
||||
auto status = noto::result_from(code, database, "reset statement");
|
||||
if (status) {
|
||||
code = sqlite3_clear_bindings(statement);
|
||||
status = noto::result_from(code, database, "clear statement bindings");
|
||||
}
|
||||
if (status) {
|
||||
status = bind_values(statement, params, database);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
auto collect_result(sqlite3_stmt *statement, sqlite3 *database, noto::execute_options const &options) -> std::expected<noto::query_result, modulus::error> {
|
||||
auto result = noto::query_result{};
|
||||
auto result_bytes = std::size_t{};
|
||||
result.columns = column_names(statement);
|
||||
auto status = track_result_columns(result.columns, result_bytes, options);
|
||||
if (!status) {
|
||||
return failure_from<noto::query_result>(reset_statement_after_execute_failure(statement, database, status));
|
||||
}
|
||||
while (true) {
|
||||
auto code = sqlite3_step(statement);
|
||||
if (code == SQLITE_ROW) {
|
||||
status = append_result_row(result, current_row(statement), result_bytes, options);
|
||||
if (!status) {
|
||||
return failure_from<noto::query_result>(reset_statement_after_execute_failure(statement, database, status));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code == SQLITE_DONE) {
|
||||
result.changes = sqlite3_changes64(database);
|
||||
result.last_insert_rowid = sqlite3_last_insert_rowid(database);
|
||||
return result;
|
||||
}
|
||||
return failure_from<noto::query_result>(noto::result_from(code, database, "execute statement"));
|
||||
}
|
||||
}
|
||||
|
||||
auto trailing_sql_is_empty(std::char_t const *tail) -> std::bool_t {
|
||||
if (tail == nullptr) {
|
||||
return true;
|
||||
}
|
||||
while (*tail != '\0') {
|
||||
if (!std::isspace(static_cast<std::uchar_t>(*tail)) && *tail != ';') {
|
||||
return false;
|
||||
}
|
||||
++tail;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector<noto::sqlite_value> const ¶ms, noto::execute_options const &options) -> std::expected<noto::query_result, modulus::error> {
|
||||
auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
|
||||
auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement");
|
||||
if (!prepared) {
|
||||
return failure_from<noto::query_result>(prepared);
|
||||
}
|
||||
if (raw_statement == nullptr) {
|
||||
return noto::query_result{
|
||||
.changes = sqlite3_changes64(database),
|
||||
.last_insert_rowid = sqlite3_last_insert_rowid(database),
|
||||
};
|
||||
}
|
||||
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
|
||||
raw_statement,
|
||||
sqlite3_finalize,
|
||||
};
|
||||
auto bound = bind_values(statement.get(), params, database);
|
||||
if (!bound) {
|
||||
return failure_from<noto::query_result>(bound);
|
||||
}
|
||||
return collect_result(statement.get(), database, options);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <limits>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
auto invalid_statement() -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_invalid_argument, "database statement is not valid");
|
||||
}
|
||||
|
||||
auto unavailable(std::string message) -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(message));
|
||||
}
|
||||
|
||||
auto invalid(std::string message) -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message));
|
||||
}
|
||||
|
||||
auto timeout(std::string message) -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_timeout, std::move(message));
|
||||
}
|
||||
|
||||
auto resource_exhausted(std::string message) -> std::expected<void, modulus::error> {
|
||||
return noto::detail::fail_void(noto::detail::error_resource_exhausted, std::move(message));
|
||||
}
|
||||
|
||||
auto contains_null(std::string_view value) -> std::bool_t {
|
||||
return value.find('\0') != std::string_view::npos;
|
||||
}
|
||||
|
||||
auto sqlite_int_max() -> std::size_t {
|
||||
return static_cast<std::size_t>(std::numeric_limits<std::int32_t>::max());
|
||||
}
|
||||
|
||||
auto validate_sqlite_milliseconds(std::chrono::milliseconds value, std::string_view label) -> std::expected<void, modulus::error> {
|
||||
if (value.count() <= 0) {
|
||||
return invalid(std::string{label} + " must be positive");
|
||||
}
|
||||
if (static_cast<std::uint64_t>(value.count()) > static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max())) {
|
||||
return invalid(std::string{label} + " is out of range");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto validate_path(std::string_view path, std::string_view label) -> std::expected<void, modulus::error> {
|
||||
if (path.empty()) {
|
||||
return invalid(std::string{label} + " path must not be empty");
|
||||
}
|
||||
if (contains_null(path)) {
|
||||
return invalid(std::string{label} + " path must not contain null bytes");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto open_flags(noto::connection_config const &config) -> std::int32_t {
|
||||
auto flags = SQLITE_OPEN_FULLMUTEX;
|
||||
switch (config.mode) {
|
||||
case noto::open_mode::READ_ONLY:
|
||||
flags |= SQLITE_OPEN_READONLY;
|
||||
break;
|
||||
case noto::open_mode::READ_WRITE:
|
||||
flags |= SQLITE_OPEN_READWRITE;
|
||||
break;
|
||||
case noto::open_mode::READ_WRITE_CREATE:
|
||||
case noto::open_mode::MEMORY:
|
||||
flags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
||||
break;
|
||||
}
|
||||
if (config.uri) {
|
||||
flags |= SQLITE_OPEN_URI;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
auto valid_token(std::string_view value) -> std::bool_t {
|
||||
if (value.empty() || contains_null(value)) {
|
||||
return false;
|
||||
}
|
||||
for (auto character : value) {
|
||||
auto byte = static_cast<std::uchar_t>(character);
|
||||
if ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z') || (byte >= '0' && byte <= '9') || character == '_') {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto transaction_mode_sql(noto::transaction_mode mode) -> std::string_view {
|
||||
switch (mode) {
|
||||
case noto::transaction_mode::DEFERRED:
|
||||
return "BEGIN DEFERRED";
|
||||
case noto::transaction_mode::IMMEDIATE:
|
||||
return "BEGIN IMMEDIATE";
|
||||
case noto::transaction_mode::EXCLUSIVE:
|
||||
return "BEGIN EXCLUSIVE";
|
||||
}
|
||||
return "BEGIN IMMEDIATE";
|
||||
}
|
||||
|
||||
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) {
|
||||
return unavailable(std::string{operation} + " requires an open database connection");
|
||||
}
|
||||
if (core.busy) {
|
||||
return unavailable("database connection already has an active operation");
|
||||
}
|
||||
core.busy = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto finish_operation(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error> {
|
||||
core.busy = false;
|
||||
if (!status && status.error().code() == noto::detail::error_data_loss) {
|
||||
core.set_state(noto::connection_state::FAILED);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <sys/types.h>
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <sqlite3.h>
|
||||
#include <modulus.hpp>
|
||||
#include <noto/config.hpp>
|
||||
#include <noto/detail/core.hpp>
|
||||
#include <noto/detail/errors.hpp>
|
||||
#include <noto/detail/sql.hpp>
|
||||
#include <noto/schema.hpp>
|
||||
#include <noto/transaction.hpp>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
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(pending_statement_finalize const &) = delete;
|
||||
auto operator=(pending_statement_finalize const &) -> pending_statement_finalize & = delete;
|
||||
~pending_statement_finalize();
|
||||
auto finish() noexcept -> void;
|
||||
std::shared_ptr<noto::detail::connection_core> owner{};
|
||||
sqlite3_stmt *statement{};
|
||||
std::bool_t counted{};
|
||||
};
|
||||
|
||||
struct pending_transaction_rollback {
|
||||
explicit pending_transaction_rollback(std::shared_ptr<noto::detail::connection_core> owner_value);
|
||||
pending_transaction_rollback(pending_transaction_rollback const &) = delete;
|
||||
auto operator=(pending_transaction_rollback const &) -> pending_transaction_rollback & = delete;
|
||||
~pending_transaction_rollback();
|
||||
auto finish() noexcept -> void;
|
||||
std::shared_ptr<noto::detail::connection_core> owner{};
|
||||
std::bool_t active{true};
|
||||
};
|
||||
|
||||
struct file_identity {
|
||||
dev_t device{};
|
||||
ino_t inode{};
|
||||
};
|
||||
|
||||
struct protected_path {
|
||||
std::filesystem::path normalized{};
|
||||
std::optional<file_identity> identity{};
|
||||
};
|
||||
|
||||
struct export_column {
|
||||
std::string quoted_name{};
|
||||
};
|
||||
|
||||
struct export_table {
|
||||
std::string quoted_name{};
|
||||
std::vector<export_column> columns{};
|
||||
};
|
||||
|
||||
struct export_schema {
|
||||
std::vector<export_table> tables{};
|
||||
std::vector<std::string> table_sql{};
|
||||
std::vector<std::string> post_data_sql{};
|
||||
std::bool_t has_sqlite_sequence{};
|
||||
};
|
||||
|
||||
auto invalid_statement() -> std::expected<void, modulus::error>;
|
||||
auto unavailable(std::string message) -> std::expected<void, modulus::error>;
|
||||
auto invalid(std::string message) -> std::expected<void, modulus::error>;
|
||||
auto timeout(std::string message) -> std::expected<void, modulus::error>;
|
||||
auto resource_exhausted(std::string message) -> std::expected<void, modulus::error>;
|
||||
auto contains_null(std::string_view value) -> std::bool_t;
|
||||
auto sqlite_int_max() -> std::size_t;
|
||||
auto validate_sqlite_milliseconds(std::chrono::milliseconds value, std::string_view label) -> std::expected<void, modulus::error>;
|
||||
auto validate_path(std::string_view path, std::string_view label) -> std::expected<void, modulus::error>;
|
||||
auto errno_status(std::string operation, std::int32_t code) -> std::expected<void, modulus::error>;
|
||||
auto filesystem_status(std::string operation, std::error_code const &code) -> std::expected<void, modulus::error>;
|
||||
auto close_fd(std::int32_t fd, 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 sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error>;
|
||||
auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected<std::string, modulus::error>;
|
||||
auto remove_file_noexcept(std::string const &path) noexcept -> void;
|
||||
auto remove_file_if_exists(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>;
|
||||
auto remove_database_files_noexcept(std::string const &path) noexcept -> void;
|
||||
auto replace_file_atomically(std::string const &temporary, std::string const &destination, std::string_view operation) -> std::expected<void, modulus::error>;
|
||||
auto validate_database_name(std::string_view name, std::string_view label) -> std::expected<void, modulus::error>;
|
||||
auto open_flags(noto::connection_config const &config) -> std::int32_t;
|
||||
auto valid_token(std::string_view value) -> std::bool_t;
|
||||
auto transaction_mode_sql(noto::transaction_mode mode) -> std::string_view;
|
||||
auto begin_operation(noto::detail::connection_core &core, std::string_view operation) -> std::expected<void, modulus::error>;
|
||||
auto finish_operation(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error>;
|
||||
auto validate_backup_options(noto::backup_options const &options) -> std::expected<void, modulus::error>;
|
||||
auto validate_export_options(noto::export_options const &options) -> std::expected<void, modulus::error>;
|
||||
auto exec_direct(sqlite3 *database, std::string const &sql, 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>;
|
||||
auto pragma_value_sql(std::variant<std::bool_t, std::int64_t, std::string> const &value) -> std::expected<std::string, 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>;
|
||||
auto open_configured_database(noto::connection_config const &config, std::chrono::milliseconds busy_timeout, sqlite3 **output) -> std::expected<void, modulus::error>;
|
||||
auto retry_timeout(noto::detail::connection_core const &core, noto::backup_options const &options) -> std::chrono::milliseconds;
|
||||
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 close_sqlite_handle(sqlite3 *handle, std::string_view operation, std::expected<void, modulus::error> status) -> 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 close_database_for_atomic_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 restore_atomic_failure(noto::detail::connection_core &core, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error>;
|
||||
auto statement_is_open(noto::detail::statement_core const &core) -> std::bool_t;
|
||||
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_values(sqlite3_stmt *statement, std::vector<noto::sqlite_value> const ¶ms, sqlite3 *database) -> std::expected<void, modulus::error>;
|
||||
auto column_value(sqlite3_stmt *statement, std::int32_t index) -> noto::sqlite_value;
|
||||
auto current_row(sqlite3_stmt *statement) -> std::vector<noto::sqlite_value>;
|
||||
auto column_names(sqlite3_stmt *statement) -> std::vector<std::string>;
|
||||
auto value_result_bytes(noto::sqlite_value const &value) -> std::size_t;
|
||||
auto add_result_bytes(std::size_t ¤t, std::size_t addition, 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>;
|
||||
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 reset_statement_after_execute_failure(sqlite3_stmt *statement, sqlite3 *database, std::expected<void, modulus::error> status) -> std::expected<void, modulus::error>;
|
||||
auto reset_and_bind(sqlite3_stmt *statement, sqlite3 *database, std::vector<noto::sqlite_value> const ¶ms) -> std::expected<void, modulus::error>;
|
||||
auto collect_result(sqlite3_stmt *statement, sqlite3 *database, noto::execute_options const &options) -> std::expected<noto::query_result, modulus::error>;
|
||||
auto trailing_sql_is_empty(std::char_t const *tail) -> std::bool_t;
|
||||
auto prepare_statement(sqlite3 *database, std::string const &sql, sqlite3_stmt **statement, std::string_view operation) -> std::expected<void, modulus::error>;
|
||||
auto same_identity(file_identity const &left, file_identity const &right) -> std::bool_t;
|
||||
auto normalized_path(std::string const &path, std::string_view operation) -> std::expected<std::filesystem::path, modulus::error>;
|
||||
auto identity_for_path(std::filesystem::path const &path, std::string_view operation) -> std::expected<std::optional<file_identity>, modulus::error>;
|
||||
auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected<protected_path, modulus::error>;
|
||||
auto same_protected_path(protected_path const &left, protected_path const &right) -> std::bool_t;
|
||||
auto live_database_paths(sqlite3 *database) -> std::expected<std::vector<std::string>, modulus::error>;
|
||||
auto validate_output_not_live_database(sqlite3 *database, std::string const &path, std::string_view label) -> std::expected<void, modulus::error>;
|
||||
auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector<noto::sqlite_value> const ¶ms, noto::execute_options const &options) -> std::expected<noto::query_result, modulus::error>;
|
||||
auto hex_literal(std::byte const *data, std::size_t size) -> std::string;
|
||||
auto text_hex_literal(std::string const &value) -> std::string;
|
||||
auto literal_sql(noto::sqlite_value const &value) -> std::expected<std::string, 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>;
|
||||
auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional<std::string>;
|
||||
auto identifier_character(std::char_t character) -> std::bool_t;
|
||||
auto consume_keyword(std::string_view sql, std::size_t &position, std::string_view keyword) -> std::bool_t;
|
||||
auto create_virtual_table_sql(std::string_view sql) -> std::bool_t;
|
||||
auto table_exists(sqlite3 *database, std::string const &table_name) -> std::expected<std::bool_t, modulus::error>;
|
||||
auto exportable_columns(sqlite3 *database, std::string const &table_name) -> std::expected<std::vector<export_column>, modulus::error>;
|
||||
auto collect_export_schema(sqlite3 *database) -> std::expected<export_schema, 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 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 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>>;
|
||||
|
||||
template <typename VALUE>
|
||||
auto failure_from(modulus::error error) -> std::expected<VALUE, modulus::error> {
|
||||
return noto::detail::fail<VALUE>(std::move(error));
|
||||
}
|
||||
|
||||
template <typename VALUE>
|
||||
auto failure_from(std::expected<void, modulus::error> status) -> std::expected<VALUE, modulus::error> {
|
||||
return noto::detail::fail<VALUE>(status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <noto/errors.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
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() {
|
||||
finish();
|
||||
}
|
||||
|
||||
auto pending_statement_finalize::finish() noexcept -> void {
|
||||
auto *current = std::exchange(statement, nullptr);
|
||||
if (current != nullptr) {
|
||||
static_cast<void>(sqlite3_finalize(current));
|
||||
}
|
||||
if (counted && owner && owner->live_statements.load(std::memory_order_acquire) > 0) {
|
||||
owner->live_statements.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
counted = false;
|
||||
}
|
||||
|
||||
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) {
|
||||
connection->live_statements.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
|
||||
noto::detail::statement_core::~statement_core() {
|
||||
if (finalized.exchange(true, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
auto *current = std::exchange(statement, nullptr);
|
||||
auto owner = connection;
|
||||
auto was_counted = counted;
|
||||
counted = false;
|
||||
if (current == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!owner) {
|
||||
static_cast<void>(sqlite3_finalize(current));
|
||||
return;
|
||||
}
|
||||
auto cleanup = std::shared_ptr<pending_statement_finalize>{};
|
||||
try {
|
||||
cleanup = std::make_shared<pending_statement_finalize>(owner, current, was_counted);
|
||||
boost::asio::post(owner->strand, [cleanup] {
|
||||
cleanup->finish();
|
||||
});
|
||||
} catch (...) {
|
||||
if (cleanup) {
|
||||
cleanup->finish();
|
||||
} else {
|
||||
auto fallback = pending_statement_finalize{
|
||||
std::move(owner),
|
||||
current,
|
||||
was_counted,
|
||||
};
|
||||
fallback.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto noto::detail::statement_core::async_reset(std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto owner = connection;
|
||||
if (!owner) {
|
||||
co_return invalid_statement();
|
||||
}
|
||||
co_await boost::asio::dispatch(owner->strand, boost::asio::use_awaitable);
|
||||
if (!statement_is_open(*this)) {
|
||||
co_return invalid_statement();
|
||||
}
|
||||
auto ready = begin_operation(*owner, "reset statement");
|
||||
if (!ready) {
|
||||
co_return ready;
|
||||
}
|
||||
auto status = reset_and_bind(statement, owner->database, params);
|
||||
co_return finish_operation(*owner, status);
|
||||
}
|
||||
|
||||
auto noto::detail::statement_core::async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>> {
|
||||
auto owner = connection;
|
||||
if (!owner) {
|
||||
co_return std::unexpected(invalid_statement().error());
|
||||
}
|
||||
co_await boost::asio::dispatch(owner->strand, boost::asio::use_awaitable);
|
||||
if (!statement_is_open(*this)) {
|
||||
co_return std::unexpected(invalid_statement().error());
|
||||
}
|
||||
auto ready = begin_operation(*owner, "step statement");
|
||||
if (!ready) {
|
||||
co_return std::unexpected(ready.error());
|
||||
}
|
||||
auto code = sqlite3_step(statement);
|
||||
if (code == SQLITE_ROW) {
|
||||
auto row = current_row(statement);
|
||||
static_cast<void>(finish_operation(*owner, {}));
|
||||
co_return std::optional<std::vector<noto::sqlite_value>>{std::move(row)};
|
||||
}
|
||||
if (code == SQLITE_DONE) {
|
||||
static_cast<void>(finish_operation(*owner, {}));
|
||||
co_return std::optional<std::vector<noto::sqlite_value>>{};
|
||||
}
|
||||
auto status = finish_operation(*owner, noto::result_from(code, owner->database, "step statement"));
|
||||
co_return std::unexpected(status.error());
|
||||
}
|
||||
|
||||
auto noto::detail::statement_core::async_execute(std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
|
||||
auto owner = connection;
|
||||
if (!owner) {
|
||||
co_return std::unexpected(invalid_statement().error());
|
||||
}
|
||||
co_await boost::asio::dispatch(owner->strand, boost::asio::use_awaitable);
|
||||
if (!statement_is_open(*this)) {
|
||||
co_return std::unexpected(invalid_statement().error());
|
||||
}
|
||||
auto ready = begin_operation(*owner, "execute statement");
|
||||
if (!ready) {
|
||||
co_return std::unexpected(ready.error());
|
||||
}
|
||||
auto status = reset_and_bind(statement, owner->database, params);
|
||||
if (!status) {
|
||||
status = finish_operation(*owner, status);
|
||||
co_return std::unexpected(status.error());
|
||||
}
|
||||
auto result = collect_result(statement, owner->database, options);
|
||||
if (!result) {
|
||||
status = finish_operation(*owner, std::unexpected(result.error()));
|
||||
co_return std::unexpected(status.error());
|
||||
}
|
||||
static_cast<void>(finish_operation(*owner, {}));
|
||||
co_return result;
|
||||
}
|
||||
|
||||
auto noto::detail::statement_core::async_finalize() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto owner = connection;
|
||||
if (!owner) {
|
||||
co_return invalid_statement();
|
||||
}
|
||||
co_await boost::asio::dispatch(owner->strand, boost::asio::use_awaitable);
|
||||
if (!statement_is_open(*this)) {
|
||||
co_return invalid_statement();
|
||||
}
|
||||
auto ready = begin_operation(*owner, "finalize statement");
|
||||
if (!ready) {
|
||||
co_return ready;
|
||||
}
|
||||
auto *current = std::exchange(statement, nullptr);
|
||||
finalized.store(true, std::memory_order_release);
|
||||
auto status = noto::result_from(sqlite3_finalize(current), owner->database, "finalize statement");
|
||||
untrack_statement(*this);
|
||||
co_return finish_operation(*owner, status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "sqlite_support.hpp"
|
||||
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
|
||||
namespace noto::detail {
|
||||
|
||||
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() {
|
||||
finish();
|
||||
}
|
||||
|
||||
auto pending_transaction_rollback::finish() noexcept -> void {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
active = false;
|
||||
if (owner && owner->database != nullptr && owner->transaction_active) {
|
||||
static_cast<void>(exec_direct(owner->database, "ROLLBACK", "rollback abandoned transaction"));
|
||||
owner->transaction_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::abandon_transaction() -> void {
|
||||
auto self = shared_from_this();
|
||||
auto cleanup = std::shared_ptr<pending_transaction_rollback>{};
|
||||
try {
|
||||
cleanup = std::make_shared<pending_transaction_rollback>(self);
|
||||
boost::asio::post(strand, [cleanup] {
|
||||
cleanup->finish();
|
||||
});
|
||||
} catch (...) {
|
||||
if (cleanup) {
|
||||
cleanup->finish();
|
||||
} else {
|
||||
auto fallback = pending_transaction_rollback{std::move(self)};
|
||||
fallback.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_begin_transaction(noto::transaction_mode mode) -> boost::asio::awaitable<std::expected<noto::transaction, modulus::error>> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "begin transaction");
|
||||
if (!ready) {
|
||||
co_return std::unexpected(ready.error());
|
||||
}
|
||||
if (transaction_active) {
|
||||
static_cast<void>(finish_operation(*this, {}));
|
||||
co_return std::unexpected(unavailable("database connection already has an active transaction").error());
|
||||
}
|
||||
auto status = exec_direct(database, std::string{transaction_mode_sql(mode)}, "begin transaction");
|
||||
status = finish_operation(*this, status);
|
||||
if (!status) {
|
||||
co_return std::unexpected(status.error());
|
||||
}
|
||||
transaction_active = true;
|
||||
co_return noto::transaction{shared_from_this()};
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_commit_transaction() -> boost::asio::awaitable<noto::detail::transaction_result> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "commit transaction");
|
||||
if (!ready) {
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = ready,
|
||||
.active = transaction_active,
|
||||
};
|
||||
}
|
||||
if (!transaction_active) {
|
||||
static_cast<void>(finish_operation(*this, {}));
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = invalid("transaction is not active"),
|
||||
.active = false,
|
||||
};
|
||||
}
|
||||
auto status = exec_direct(database, "COMMIT", "commit transaction");
|
||||
if (status) {
|
||||
transaction_active = false;
|
||||
}
|
||||
status = finish_operation(*this, status);
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = status,
|
||||
.active = transaction_active,
|
||||
};
|
||||
}
|
||||
|
||||
auto noto::detail::connection_core::async_rollback_transaction() -> boost::asio::awaitable<noto::detail::transaction_result> {
|
||||
auto self = shared_from_this();
|
||||
co_await boost::asio::dispatch(strand, boost::asio::use_awaitable);
|
||||
auto ready = begin_operation(*this, "rollback transaction");
|
||||
if (!ready) {
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = ready,
|
||||
.active = transaction_active,
|
||||
};
|
||||
}
|
||||
if (!transaction_active) {
|
||||
static_cast<void>(finish_operation(*this, {}));
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = invalid("transaction is not active"),
|
||||
.active = false,
|
||||
};
|
||||
}
|
||||
auto status = exec_direct(database, "ROLLBACK", "rollback transaction");
|
||||
if (status) {
|
||||
transaction_active = false;
|
||||
}
|
||||
status = finish_operation(*this, status);
|
||||
co_return noto::detail::transaction_result{
|
||||
.status = status,
|
||||
.active = transaction_active,
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!on_finish) {
|
||||
return;
|
||||
}
|
||||
auto callback = std::move(on_finish);
|
||||
on_finish = {};
|
||||
callback();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user