diff --git a/CMakeLists.txt b/CMakeLists.txt index 18c43b3..407c62b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index 8d22a12..c9908ae 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/detail/core.cpp b/src/detail/core.cpp index 496a3e4..aaa201b 100644 --- a/src/detail/core.cpp +++ b/src/detail/core.cpp @@ -1,1417 +1,14 @@ -#include +#include "sqlite_support.hpp" + +#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include #include -#include -#include #include #include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - - -namespace { - -struct pending_statement_finalize { - pending_statement_finalize(std::shared_ptr 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 owner{}; - sqlite3_stmt *statement{}; - std::bool_t counted{}; -}; - -struct pending_transaction_rollback { - explicit pending_transaction_rollback(std::shared_ptr 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 owner{}; - std::bool_t active{true}; -}; - -struct file_identity { - dev_t device{}; - ino_t inode{}; -}; - -struct protected_path { - std::filesystem::path normalized{}; - std::optional identity{}; -}; - -struct export_column { - std::string quoted_name{}; -}; - -struct export_table { - std::string quoted_name{}; - std::vector columns{}; -}; - -struct export_schema { - std::vector tables{}; - std::vector table_sql{}; - std::vector post_data_sql{}; - std::bool_t has_sqlite_sequence{}; -}; - -auto invalid_statement() -> std::expected { - return noto::detail::fail_void(noto::detail::error_invalid_argument, "database statement is not valid"); -} - -auto unavailable(std::string message) -> std::expected { - return noto::detail::fail_void(noto::detail::error_unavailable, std::move(message)); -} - -auto invalid(std::string message) -> std::expected { - return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message)); -} - -auto timeout(std::string message) -> std::expected { - return noto::detail::fail_void(noto::detail::error_timeout, std::move(message)); -} - -auto resource_exhausted(std::string message) -> std::expected { - return noto::detail::fail_void(noto::detail::error_resource_exhausted, std::move(message)); -} - -template -auto failure_from(modulus::error error) -> std::expected { - return noto::detail::fail(std::move(error)); -} - -template -auto failure_from(std::expected status) -> std::expected { - return noto::detail::fail(status); -} - -auto contains_null(std::string_view value) -> std::bool_t { - return value.find('\0') != std::string_view::npos; -} - -constexpr auto sqlite_sidecar_suffixes = std::array{"-wal", "-shm", "-journal"}; - -auto sqlite_int_max() -> std::size_t { - return static_cast(std::numeric_limits::max()); -} - -auto validate_sqlite_milliseconds(std::chrono::milliseconds value, std::string_view label) -> std::expected { - if (value.count() <= 0) { - return invalid(std::string{label} + " must be positive"); - } - if (static_cast(value.count()) > static_cast(std::numeric_limits::max())) { - return invalid(std::string{label} + " is out of range"); - } - return {}; -} - -auto validate_path(std::string_view path, std::string_view label) -> std::expected { - 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 errno_status(std::string operation, std::int32_t code) -> std::expected { - 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 { - 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 { - 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 { - 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(::close(fd)); - return status; - } - return close_fd(fd, operation); -} - -auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected { - 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(::close(fd)); - return status; - } - return close_fd(fd, operation); -} - -auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected { - 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{ - 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(::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(::unlink(path.c_str())); - } -} - -auto remove_file_if_exists(std::string const &path, std::string_view operation) -> std::expected { - 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 { - 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 { - 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 validate_database_name(std::string_view name, std::string_view label) -> std::expected { - 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 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(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 { - 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 status) -> std::expected { - core.busy = false; - if (!status && status.error().code() == noto::detail::error_data_loss) { - core.set_state(noto::connection_state::FAILED); - } - return status; -} - -auto validate_backup_options(noto::backup_options const &options) -> std::expected { - 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 { - if (options.rows_per_yield == 0) { - return invalid("SQL export rows per yield must be positive"); - } - return {}; -} - -auto exec_direct(sqlite3 *database, std::string const &sql, std::string_view operation) -> std::expected { - auto *error = static_cast(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 { - return noto::result_from(sqlite3_busy_timeout(database, static_cast(timeout.count())), database, operation); -} - -auto pragma_value_sql(std::variant const &value) -> std::expected { - return std::visit([](auto const ¤t) -> std::expected { - if constexpr (std::is_same_v, std::bool_t>) { - return std::string{current ? "1" : "0"}; - } else if constexpr (std::is_same_v, 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 const &value) -> std::expected { - 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 { - *output = nullptr; - auto *next_database = static_cast(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 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> { - 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(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 status) -> std::expected { - 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> { - auto *source = static_cast(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(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 { - 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 { - auto *reopened = static_cast(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 status) -> std::expected { - 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 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 { - auto size = std::visit([](auto const ¤t) -> std::size_t { - if constexpr (std::is_same_v, std::string> || std::is_same_v, std::vector>) { - 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::monostate>) { - return sqlite3_bind_null(statement, index); - } else if constexpr (std::is_same_v, std::int64_t>) { - return sqlite3_bind_int64(statement, index, static_cast(current)); - } else if constexpr (std::is_same_v, std::float64_t>) { - return sqlite3_bind_double(statement, index, current); - } else if constexpr (std::is_same_v, std::string>) { - return sqlite3_bind_text(statement, index, current.data(), static_cast(current.size()), SQLITE_TRANSIENT); - } else { - return sqlite3_bind_blob(statement, index, current.data(), static_cast(current.size()), SQLITE_TRANSIENT); - } - }, - value.value); - return noto::result_from(code, database, "bind parameter"); -} - -auto bind_values(sqlite3_stmt *statement, std::vector const ¶ms, sqlite3 *database) -> std::expected { - 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(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(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(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(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(bytes), - }, - }; - } - case SQLITE_BLOB: { - auto const *data = static_cast(sqlite3_column_blob(statement, index)); - auto bytes = sqlite3_column_bytes(statement, index); - auto blob = std::vector{}; - 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 { - auto count = sqlite3_column_count(statement); - auto row = std::vector{}; - row.reserve(static_cast(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 { - auto count = sqlite3_column_count(statement); - auto names = std::vector{}; - names.reserve(static_cast(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::monostate>) { - return {}; - } else if constexpr (std::is_same_v, std::int64_t>) { - return sizeof(std::int64_t); - } else if constexpr (std::is_same_v, 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 { - if (!options.max_result_bytes) { - if (addition > std::numeric_limits::max() - current) { - current = std::numeric_limits::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 const &columns, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected { - 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 row, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected { - 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 status) -> std::expected { - 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 const ¶ms) -> std::expected { - 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 { - 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(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(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::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(*tail)) && *tail != ';') { - return false; - } - ++tail; - } - return true; -} - -auto prepare_statement(sqlite3 *database, std::string const &sql, sqlite3_stmt **statement, std::string_view operation) -> std::expected { - auto const *tail = static_cast(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 { - 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, 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{identity}; - } - if (errno == ENOENT || errno == ENOTDIR) { - return std::optional{}; - } - return std::unexpected(errno_status(std::string{operation}, errno).error()); -} - -auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected { - 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, modulus::error> { - auto *raw_statement = static_cast(nullptr); - auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list"); - if (!prepared) { - return failure_from>(prepared); - } - auto statement = std::unique_ptr{ - raw_statement, - sqlite3_finalize, - }; - auto paths = std::vector{}; - while (true) { - auto code = sqlite3_step(statement.get()); - if (code == SQLITE_DONE) { - return paths; - } - if (code != SQLITE_ROW) { - return failure_from>(noto::result_from(code, database, "collect open database list")); - } - if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) { - continue; - } - auto const *text = reinterpret_cast(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(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 { - 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 {}; -} - -auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector const ¶ms, noto::execute_options const &options) -> std::expected { - auto *raw_statement = static_cast(nullptr); - auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement"); - if (!prepared) { - return failure_from(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{ - raw_statement, - sqlite3_finalize, - }; - auto bound = bind_values(statement.get(), params, database); - if (!bound) { - return failure_from(bound); - } - return collect_result(statement.get(), database, options); -} - -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::to_integer(data[index])); - } - sql << "'"; - return sql.str(); -} - -auto text_hex_literal(std::string const &value) -> std::string { - auto const *bytes = reinterpret_cast(value.data()); - return "CAST(" + hex_literal(bytes, value.size()) + " AS TEXT)"; -} - -auto literal_sql(noto::sqlite_value const &value) -> std::expected { - return std::visit([](auto const ¤t) -> std::expected { - if constexpr (std::is_same_v, std::monostate>) { - return "NULL"; - } else if constexpr (std::is_same_v, std::int64_t>) { - return std::to_string(current); - } else if constexpr (std::is_same_v, std::float64_t>) { - if (!std::isfinite(current)) { - return noto::detail::fail(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::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 { - if (text.size() > static_cast(std::numeric_limits::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(text.size())); - if (!output) { - return unavailable("failed to write SQL export output"); - } - if (text.size() > std::numeric_limits::max() - written) { - written = std::numeric_limits::max(); - } else { - written += text.size(); - } - return {}; -} - -auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional { - if (sqlite3_column_type(statement, index) != SQLITE_TEXT) { - return std::nullopt; - } - auto const *text = reinterpret_cast(sqlite3_column_text(statement, index)); - auto bytes = sqlite3_column_bytes(statement, index); - return text == nullptr ? std::string{} : std::string{ - text, - static_cast(bytes), - }; -} - -auto identifier_character(std::char_t character) -> std::bool_t { - auto byte = static_cast(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(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(sql[current + index]); - auto right = static_cast(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 { - auto *raw_statement = static_cast(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(prepared); - } - auto statement = std::unique_ptr{ - raw_statement, - sqlite3_finalize, - }; - auto code = sqlite3_bind_text(statement.get(), 1, table_name.data(), static_cast(table_name.size()), SQLITE_TRANSIENT); - auto status = noto::result_from(code, database, "bind table existence check"); - if (!status) { - return failure_from(status); - } - code = sqlite3_step(statement.get()); - if (code == SQLITE_ROW) { - return true; - } - if (code == SQLITE_DONE) { - return false; - } - return failure_from(noto::result_from(code, database, "check table existence")); -} - -auto exportable_columns(sqlite3 *database, std::string const &table_name) -> std::expected, modulus::error> { - auto quoted_table = noto::quote_identifier(table_name); - if (!quoted_table) { - return failure_from>(quoted_table.error()); - } - auto *raw_statement = static_cast(nullptr); - auto prepared = prepare_statement(database, "PRAGMA table_xinfo(" + *quoted_table + ")", &raw_statement, "prepare export columns"); - if (!prepared) { - return failure_from>(prepared); - } - auto statement = std::unique_ptr{ - raw_statement, - sqlite3_finalize, - }; - if (sqlite3_column_count(statement.get()) < 7) { - return noto::detail::fail>(noto::detail::error_unavailable, "SQLite table_xinfo did not return hidden column metadata"); - } - auto columns = std::vector{}; - while (true) { - auto code = sqlite3_step(statement.get()); - if (code == SQLITE_DONE) { - return columns; - } - if (code != SQLITE_ROW) { - return failure_from>(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>(quoted_name.error()); - } - columns.push_back(export_column{ - .quoted_name = std::move(*quoted_name), - }); - } -} - -auto collect_export_schema(sqlite3 *database) -> std::expected { - auto *raw_statement = static_cast(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(prepared); - } - auto statement = std::unique_ptr{ - 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(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(quoted_name.error()); - } - auto columns = exportable_columns(database, *name); - if (!columns) { - return failure_from(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(sequence_exists.error()); - } - schema.has_sqlite_sequence = *sequence_exists; - return schema; - } - return failure_from(noto::result_from(code, database, "collect export schema")); - } -} - -auto export_table_rows(noto::detail::connection_core &core, export_table const &table, std::function(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { - auto *raw_statement = static_cast(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{ - 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{}; - } - 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(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::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { - auto status = write("DELETE FROM \"sqlite_sequence\";\n"); - if (!status) { - co_return status; - } - auto *raw_statement = static_cast(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{ - 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{}; - } - 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::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { - 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"); -} - -pending_statement_finalize::pending_statement_finalize(std::shared_ptr 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(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; -} - -pending_transaction_rollback::pending_transaction_rollback(std::shared_ptr 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(exec_direct(owner->database, "ROLLBACK", "rollback abandoned transaction")); - owner->transaction_active = false; - } -} - -} +namespace noto::detail { 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} {} @@ -1435,24 +32,6 @@ auto noto::detail::connection_core::mark_failed(std::string) -> void { set_state(noto::connection_state::FAILED); } -auto noto::detail::connection_core::abandon_transaction() -> void { - auto self = shared_from_this(); - auto cleanup = std::shared_ptr{}; - try { - cleanup = std::make_shared(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_connect() -> boost::asio::awaitable> { auto self = shared_from_this(); co_await boost::asio::dispatch(strand, boost::asio::use_awaitable); @@ -1552,294 +131,6 @@ auto noto::detail::connection_core::async_prepare(std::string sql) -> boost::asi co_return noto::statement{std::make_shared(shared_from_this(), raw_statement)}; } -auto noto::detail::connection_core::async_begin_transaction(noto::transaction_mode mode) -> boost::asio::awaitable> { - 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(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 { - 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(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 { - 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(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, - }; -} - -auto noto::detail::connection_core::async_backup_to_file(std::string path, noto::backup_options options) -> boost::asio::awaitable> { - 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(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 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> { - 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); -} - -auto noto::detail::connection_core::async_export_sql(noto::export_options options) -> boost::asio::awaitable> { - 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(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> { - 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); -} - auto noto::detail::connection_core::async_set_busy_timeout(std::chrono::milliseconds timeout) -> boost::asio::awaitable> { auto timeout_status = validate_sqlite_milliseconds(timeout, "database busy timeout"); if (!timeout_status) { @@ -1858,147 +149,4 @@ auto noto::detail::connection_core::async_set_busy_timeout(std::chrono::millisec co_return finish_operation(*this, status); } -noto::detail::statement_core::statement_core(std::shared_ptr 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(sqlite3_finalize(current)); - return; - } - auto cleanup = std::shared_ptr{}; - try { - cleanup = std::make_shared(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 params) -> boost::asio::awaitable> { - 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>, 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(finish_operation(*owner, {})); - co_return std::optional>{std::move(row)}; - } - if (code == SQLITE_DONE) { - static_cast(finish_operation(*owner, {})); - co_return std::optional>{}; - } - 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 params, noto::execute_options options) -> boost::asio::awaitable> { - 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(finish_operation(*owner, {})); - co_return result; -} - -auto noto::detail::statement_core::async_finalize() -> boost::asio::awaitable> { - 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); -} - -noto::detail::transaction_state::transaction_state(std::shared_ptr core_value, std::function 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(); } diff --git a/src/detail/sqlite_backup.cpp b/src/detail/sqlite_backup.cpp new file mode 100644 index 0000000..641a554 --- /dev/null +++ b/src/detail/sqlite_backup.cpp @@ -0,0 +1,309 @@ +#include "sqlite_support.hpp" + +#include + +#include +#include +#include +#include +#include + + +namespace noto::detail { + +auto validate_backup_options(noto::backup_options const &options) -> std::expected { + 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 { + 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> { + 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(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 status) -> std::expected { + 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> { + auto *source = static_cast(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(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 { + 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 { + auto *reopened = static_cast(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 status) -> std::expected { + 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> { + 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(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 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> { + 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); +} + +} diff --git a/src/detail/sqlite_export.cpp b/src/detail/sqlite_export.cpp new file mode 100644 index 0000000..2eb1684 --- /dev/null +++ b/src/detail/sqlite_export.cpp @@ -0,0 +1,491 @@ +#include "sqlite_support.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +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::to_integer(data[index])); + } + sql << "'"; + return sql.str(); +} + +auto text_hex_literal(std::string const &value) -> std::string { + auto const *bytes = reinterpret_cast(value.data()); + return "CAST(" + hex_literal(bytes, value.size()) + " AS TEXT)"; +} + +auto literal_sql(noto::sqlite_value const &value) -> std::expected { + return std::visit([](auto const ¤t) -> std::expected { + if constexpr (std::is_same_v, std::monostate>) { + return "NULL"; + } else if constexpr (std::is_same_v, std::int64_t>) { + return std::to_string(current); + } else if constexpr (std::is_same_v, std::float64_t>) { + if (!std::isfinite(current)) { + return noto::detail::fail(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::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 { + if (text.size() > static_cast(std::numeric_limits::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(text.size())); + if (!output) { + return unavailable("failed to write SQL export output"); + } + if (text.size() > std::numeric_limits::max() - written) { + written = std::numeric_limits::max(); + } else { + written += text.size(); + } + return {}; +} + +auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional { + if (sqlite3_column_type(statement, index) != SQLITE_TEXT) { + return std::nullopt; + } + auto const *text = reinterpret_cast(sqlite3_column_text(statement, index)); + auto bytes = sqlite3_column_bytes(statement, index); + return text == nullptr ? std::string{} : std::string{ + text, + static_cast(bytes), + }; +} + +auto identifier_character(std::char_t character) -> std::bool_t { + auto byte = static_cast(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(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(sql[current + index]); + auto right = static_cast(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 { + auto *raw_statement = static_cast(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(prepared); + } + auto statement = std::unique_ptr{ + raw_statement, + sqlite3_finalize, + }; + auto code = sqlite3_bind_text(statement.get(), 1, table_name.data(), static_cast(table_name.size()), SQLITE_TRANSIENT); + auto status = noto::result_from(code, database, "bind table existence check"); + if (!status) { + return failure_from(status); + } + code = sqlite3_step(statement.get()); + if (code == SQLITE_ROW) { + return true; + } + if (code == SQLITE_DONE) { + return false; + } + return failure_from(noto::result_from(code, database, "check table existence")); +} + +auto exportable_columns(sqlite3 *database, std::string const &table_name) -> std::expected, modulus::error> { + auto quoted_table = noto::quote_identifier(table_name); + if (!quoted_table) { + return failure_from>(quoted_table.error()); + } + auto *raw_statement = static_cast(nullptr); + auto prepared = prepare_statement(database, "PRAGMA table_xinfo(" + *quoted_table + ")", &raw_statement, "prepare export columns"); + if (!prepared) { + return failure_from>(prepared); + } + auto statement = std::unique_ptr{ + raw_statement, + sqlite3_finalize, + }; + if (sqlite3_column_count(statement.get()) < 7) { + return noto::detail::fail>(noto::detail::error_unavailable, "SQLite table_xinfo did not return hidden column metadata"); + } + auto columns = std::vector{}; + while (true) { + auto code = sqlite3_step(statement.get()); + if (code == SQLITE_DONE) { + return columns; + } + if (code != SQLITE_ROW) { + return failure_from>(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>(quoted_name.error()); + } + columns.push_back(export_column{ + .quoted_name = std::move(*quoted_name), + }); + } +} + +auto collect_export_schema(sqlite3 *database) -> std::expected { + auto *raw_statement = static_cast(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(prepared); + } + auto statement = std::unique_ptr{ + 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(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(quoted_name.error()); + } + auto columns = exportable_columns(database, *name); + if (!columns) { + return failure_from(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(sequence_exists.error()); + } + schema.has_sqlite_sequence = *sequence_exists; + return schema; + } + return failure_from(noto::result_from(code, database, "collect export schema")); + } +} + +auto export_table_rows(noto::detail::connection_core &core, export_table const &table, std::function(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { + auto *raw_statement = static_cast(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{ + 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{}; + } + 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(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::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { + auto status = write("DELETE FROM \"sqlite_sequence\";\n"); + if (!status) { + co_return status; + } + auto *raw_statement = static_cast(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{ + 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{}; + } + 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::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable> { + 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> { + 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(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> { + 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); +} + +} diff --git a/src/detail/sqlite_files.cpp b/src/detail/sqlite_files.cpp new file mode 100644 index 0000000..c18046f --- /dev/null +++ b/src/detail/sqlite_files.cpp @@ -0,0 +1,275 @@ +#include "sqlite_support.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + + +namespace noto::detail { + +constexpr auto sqlite_sidecar_suffixes = std::array{"-wal", "-shm", "-journal"}; + +auto errno_status(std::string operation, std::int32_t code) -> std::expected { + 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 { + 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 { + 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 { + 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(::close(fd)); + return status; + } + return close_fd(fd, operation); +} + +auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected { + 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(::close(fd)); + return status; + } + return close_fd(fd, operation); +} + +auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected { + 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{ + 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(::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(::unlink(path.c_str())); + } +} + +auto remove_file_if_exists(std::string const &path, std::string_view operation) -> std::expected { + 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 { + 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 { + 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 { + auto const *tail = static_cast(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 { + 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, 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{identity}; + } + if (errno == ENOENT || errno == ENOTDIR) { + return std::optional{}; + } + return std::unexpected(errno_status(std::string{operation}, errno).error()); +} + +auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected { + 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, modulus::error> { + auto *raw_statement = static_cast(nullptr); + auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list"); + if (!prepared) { + return failure_from>(prepared); + } + auto statement = std::unique_ptr{ + raw_statement, + sqlite3_finalize, + }; + auto paths = std::vector{}; + while (true) { + auto code = sqlite3_step(statement.get()); + if (code == SQLITE_DONE) { + return paths; + } + if (code != SQLITE_ROW) { + return failure_from>(noto::result_from(code, database, "collect open database list")); + } + if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) { + continue; + } + auto const *text = reinterpret_cast(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(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 { + 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 {}; +} + +} diff --git a/src/detail/sqlite_query.cpp b/src/detail/sqlite_query.cpp new file mode 100644 index 0000000..a6738f1 --- /dev/null +++ b/src/detail/sqlite_query.cpp @@ -0,0 +1,346 @@ +#include "sqlite_support.hpp" + +#include + +#include +#include +#include +#include + + +namespace noto::detail { + +auto validate_database_name(std::string_view name, std::string_view label) -> std::expected { + 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 { + auto *error = static_cast(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 { + return noto::result_from(sqlite3_busy_timeout(database, static_cast(timeout.count())), database, operation); +} + +auto pragma_value_sql(std::variant const &value) -> std::expected { + return std::visit([](auto const ¤t) -> std::expected { + if constexpr (std::is_same_v, std::bool_t>) { + return std::string{current ? "1" : "0"}; + } else if constexpr (std::is_same_v, 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 const &value) -> std::expected { + 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 { + *output = nullptr; + auto *next_database = static_cast(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 { + auto size = std::visit([](auto const ¤t) -> std::size_t { + if constexpr (std::is_same_v, std::string> || std::is_same_v, std::vector>) { + 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::monostate>) { + return sqlite3_bind_null(statement, index); + } else if constexpr (std::is_same_v, std::int64_t>) { + return sqlite3_bind_int64(statement, index, static_cast(current)); + } else if constexpr (std::is_same_v, std::float64_t>) { + return sqlite3_bind_double(statement, index, current); + } else if constexpr (std::is_same_v, std::string>) { + return sqlite3_bind_text(statement, index, current.data(), static_cast(current.size()), SQLITE_TRANSIENT); + } else { + return sqlite3_bind_blob(statement, index, current.data(), static_cast(current.size()), SQLITE_TRANSIENT); + } + }, + value.value); + return noto::result_from(code, database, "bind parameter"); +} + +auto bind_values(sqlite3_stmt *statement, std::vector const ¶ms, sqlite3 *database) -> std::expected { + 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(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(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(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(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(bytes), + }, + }; + } + case SQLITE_BLOB: { + auto const *data = static_cast(sqlite3_column_blob(statement, index)); + auto bytes = sqlite3_column_bytes(statement, index); + auto blob = std::vector{}; + 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 { + auto count = sqlite3_column_count(statement); + auto row = std::vector{}; + row.reserve(static_cast(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 { + auto count = sqlite3_column_count(statement); + auto names = std::vector{}; + names.reserve(static_cast(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::monostate>) { + return {}; + } else if constexpr (std::is_same_v, std::int64_t>) { + return sizeof(std::int64_t); + } else if constexpr (std::is_same_v, 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 { + if (!options.max_result_bytes) { + if (addition > std::numeric_limits::max() - current) { + current = std::numeric_limits::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 const &columns, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected { + 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 row, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected { + 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 status) -> std::expected { + 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 const ¶ms) -> std::expected { + 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 { + 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(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(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::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(*tail)) && *tail != ';') { + return false; + } + ++tail; + } + return true; +} + +auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector const ¶ms, noto::execute_options const &options) -> std::expected { + auto *raw_statement = static_cast(nullptr); + auto prepared = prepare_statement(database, sql, &raw_statement, "prepare statement"); + if (!prepared) { + return failure_from(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{ + raw_statement, + sqlite3_finalize, + }; + auto bound = bind_values(statement.get(), params, database); + if (!bound) { + return failure_from(bound); + } + return collect_result(statement.get(), database, options); +} + +} diff --git a/src/detail/sqlite_support.cpp b/src/detail/sqlite_support.cpp new file mode 100644 index 0000000..49edd39 --- /dev/null +++ b/src/detail/sqlite_support.cpp @@ -0,0 +1,122 @@ +#include "sqlite_support.hpp" + +#include +#include + + +namespace noto::detail { + +auto invalid_statement() -> std::expected { + return noto::detail::fail_void(noto::detail::error_invalid_argument, "database statement is not valid"); +} + +auto unavailable(std::string message) -> std::expected { + return noto::detail::fail_void(noto::detail::error_unavailable, std::move(message)); +} + +auto invalid(std::string message) -> std::expected { + return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message)); +} + +auto timeout(std::string message) -> std::expected { + return noto::detail::fail_void(noto::detail::error_timeout, std::move(message)); +} + +auto resource_exhausted(std::string message) -> std::expected { + 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::numeric_limits::max()); +} + +auto validate_sqlite_milliseconds(std::chrono::milliseconds value, std::string_view label) -> std::expected { + if (value.count() <= 0) { + return invalid(std::string{label} + " must be positive"); + } + if (static_cast(value.count()) > static_cast(std::numeric_limits::max())) { + return invalid(std::string{label} + " is out of range"); + } + return {}; +} + +auto validate_path(std::string_view path, std::string_view label) -> std::expected { + 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(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 { + 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 status) -> std::expected { + core.busy = false; + if (!status && status.error().code() == noto::detail::error_data_loss) { + core.set_state(noto::connection_state::FAILED); + } + return status; +} + +} diff --git a/src/detail/sqlite_support.hpp b/src/detail/sqlite_support.hpp new file mode 100644 index 0000000..5936051 --- /dev/null +++ b/src/detail/sqlite_support.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace noto::detail { + +struct pending_statement_finalize { + pending_statement_finalize(std::shared_ptr 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 owner{}; + sqlite3_stmt *statement{}; + std::bool_t counted{}; +}; + +struct pending_transaction_rollback { + explicit pending_transaction_rollback(std::shared_ptr 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 owner{}; + std::bool_t active{true}; +}; + +struct file_identity { + dev_t device{}; + ino_t inode{}; +}; + +struct protected_path { + std::filesystem::path normalized{}; + std::optional identity{}; +}; + +struct export_column { + std::string quoted_name{}; +}; + +struct export_table { + std::string quoted_name{}; + std::vector columns{}; +}; + +struct export_schema { + std::vector tables{}; + std::vector table_sql{}; + std::vector post_data_sql{}; + std::bool_t has_sqlite_sequence{}; +}; + +auto invalid_statement() -> std::expected; +auto unavailable(std::string message) -> std::expected; +auto invalid(std::string message) -> std::expected; +auto timeout(std::string message) -> std::expected; +auto resource_exhausted(std::string message) -> std::expected; +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; +auto validate_path(std::string_view path, std::string_view label) -> std::expected; +auto errno_status(std::string operation, std::int32_t code) -> std::expected; +auto filesystem_status(std::string operation, std::error_code const &code) -> std::expected; +auto close_fd(std::int32_t fd, std::string_view operation) -> std::expected; +auto sync_file_path(std::string const &path, std::string_view operation) -> std::expected; +auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected; +auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected; +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; +auto remove_database_sidecars(std::string const &path, std::string_view operation) -> std::expected; +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; +auto validate_database_name(std::string_view name, std::string_view label) -> std::expected; +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; +auto finish_operation(noto::detail::connection_core &core, std::expected status) -> std::expected; +auto validate_backup_options(noto::backup_options const &options) -> std::expected; +auto validate_export_options(noto::export_options const &options) -> std::expected; +auto exec_direct(sqlite3 *database, std::string const &sql, std::string_view operation) -> std::expected; +auto configure_busy_timeout(sqlite3 *database, std::chrono::milliseconds timeout, std::string_view operation) -> std::expected; +auto pragma_value_sql(std::variant const &value) -> std::expected; +auto set_pragma(sqlite3 *database, std::string const &name, std::variant const &value) -> std::expected; +auto open_configured_database(noto::connection_config const &config, std::chrono::milliseconds busy_timeout, sqlite3 **output) -> std::expected; +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>; +auto close_sqlite_handle(sqlite3 *handle, std::string_view operation, std::expected status) -> std::expected; +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>; +auto close_database_for_atomic_restore(noto::detail::connection_core &core) -> std::expected; +auto reopen_database_after_restore(noto::detail::connection_core &core) -> std::expected; +auto restore_atomic_failure(noto::detail::connection_core &core, std::expected status) -> std::expected; +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; +auto bind_values(sqlite3_stmt *statement, std::vector const ¶ms, sqlite3 *database) -> std::expected; +auto column_value(sqlite3_stmt *statement, std::int32_t index) -> noto::sqlite_value; +auto current_row(sqlite3_stmt *statement) -> std::vector; +auto column_names(sqlite3_stmt *statement) -> std::vector; +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; +auto track_result_columns(std::vector const &columns, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected; +auto append_result_row(noto::query_result &result, std::vector row, std::size_t &result_bytes, noto::execute_options const &options) -> std::expected; +auto reset_statement_after_execute_failure(sqlite3_stmt *statement, sqlite3 *database, std::expected status) -> std::expected; +auto reset_and_bind(sqlite3_stmt *statement, sqlite3 *database, std::vector const ¶ms) -> std::expected; +auto collect_result(sqlite3_stmt *statement, sqlite3 *database, noto::execute_options const &options) -> std::expected; +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; +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; +auto identity_for_path(std::filesystem::path const &path, std::string_view operation) -> std::expected, modulus::error>; +auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected; +auto same_protected_path(protected_path const &left, protected_path const &right) -> std::bool_t; +auto live_database_paths(sqlite3 *database) -> std::expected, modulus::error>; +auto validate_output_not_live_database(sqlite3 *database, std::string const &path, std::string_view label) -> std::expected; +auto execute_prepared(sqlite3 *database, std::string const &sql, std::vector const ¶ms, noto::execute_options const &options) -> std::expected; +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; +auto write_limited(std::ostream &output, std::string_view text, noto::export_options const &options, std::size_t &written) -> std::expected; +auto column_text(sqlite3_stmt *statement, std::int32_t index) -> std::optional; +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; +auto exportable_columns(sqlite3 *database, std::string const &table_name) -> std::expected, modulus::error>; +auto collect_export_schema(sqlite3 *database) -> std::expected; +auto export_table_rows(noto::detail::connection_core &core, export_table const &table, std::function(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable>; +auto export_sqlite_sequence(noto::detail::connection_core &core, std::function(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable>; +auto write_export(noto::detail::connection_core &core, std::function(std::string_view)> const &write, noto::export_options const &options) -> boost::asio::awaitable>; + +template +auto failure_from(modulus::error error) -> std::expected { + return noto::detail::fail(std::move(error)); +} + +template +auto failure_from(std::expected status) -> std::expected { + return noto::detail::fail(status); +} + +} diff --git a/src/detail/statement_core.cpp b/src/detail/statement_core.cpp new file mode 100644 index 0000000..4cc3e97 --- /dev/null +++ b/src/detail/statement_core.cpp @@ -0,0 +1,164 @@ +#include "sqlite_support.hpp" + +#include + +#include +#include +#include +#include + + +namespace noto::detail { + +pending_statement_finalize::pending_statement_finalize(std::shared_ptr 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(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 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(sqlite3_finalize(current)); + return; + } + auto cleanup = std::shared_ptr{}; + try { + cleanup = std::make_shared(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 params) -> boost::asio::awaitable> { + 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>, 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(finish_operation(*owner, {})); + co_return std::optional>{std::move(row)}; + } + if (code == SQLITE_DONE) { + static_cast(finish_operation(*owner, {})); + co_return std::optional>{}; + } + 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 params, noto::execute_options options) -> boost::asio::awaitable> { + 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(finish_operation(*owner, {})); + co_return result; +} + +auto noto::detail::statement_core::async_finalize() -> boost::asio::awaitable> { + 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); +} + +} diff --git a/src/detail/transaction_core.cpp b/src/detail/transaction_core.cpp new file mode 100644 index 0000000..f8439c5 --- /dev/null +++ b/src/detail/transaction_core.cpp @@ -0,0 +1,134 @@ +#include "sqlite_support.hpp" + +#include +#include +#include + + +namespace noto::detail { + +pending_transaction_rollback::pending_transaction_rollback(std::shared_ptr 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(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{}; + try { + cleanup = std::make_shared(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> { + 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(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 { + 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(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 { + 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(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 core_value, std::function 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(); +} + +}