Cleanup
publish / publish (push) Successful in 16m12s

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