Initial public khodo snapshot
publish / publish (push) Successful in 26m25s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit fc0bad81c4
31 changed files with 6668 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
#include <noto/config.hpp>
#include <chrono>
#include <cstdint>
#include <expected>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
namespace {
constexpr auto json_options = modulus::json_config::options{
.category = "noto",
.invalid_argument_code = noto::detail::error_invalid_argument,
.not_found_code = noto::detail::error_not_found
};
auto invalid(std::string message) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message));
}
template <typename VALUE>
auto invalid_result(std::string message) -> std::expected<VALUE, modulus::error> {
return noto::detail::fail<VALUE>(noto::detail::error_invalid_argument, 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::uint64_t {
return static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max());
}
auto valid_pragma_name(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 mode_name(noto::open_mode mode) -> std::string_view {
switch (mode) {
case noto::open_mode::READ_ONLY:
return "read_only";
case noto::open_mode::READ_WRITE:
return "read_write";
case noto::open_mode::READ_WRITE_CREATE:
return "read_write_create";
case noto::open_mode::MEMORY:
return "memory";
}
return "read_write_create";
}
auto required_string_field(boost::json::object const& object, std::string_view name) -> std::expected<std::string, modulus::error> {
auto value = modulus::json_config::required_string_field(object, name, "database", json_options);
if (!value) {
return noto::detail::fail<std::string>(value.error());
}
if (contains_null(*value)) {
return invalid_result<std::string>(std::string{"database "} + std::string{name} + " must not contain null bytes");
}
return std::move(*value);
}
auto optional_string_field(boost::json::object const& object, std::string_view name) -> std::expected<std::optional<std::string>, modulus::error> {
auto found = object.find(name);
if (found == object.end()) {
return std::optional<std::string>{};
}
auto parsed = modulus::json_config::optional_string_field(object, name, {}, json_options);
if (!parsed) {
return noto::detail::fail<std::optional<std::string>>(parsed.error());
}
auto value = std::move(*parsed);
if (value.empty()) {
return invalid_result<std::optional<std::string>>(std::string{"database "} + std::string{name} + " must not be empty");
}
if (contains_null(value)) {
return invalid_result<std::optional<std::string>>(std::string{"database "} + std::string{name} + " must not contain null bytes");
}
return std::optional<std::string>{std::move(value)};
}
auto parse_pragma_value(boost::json::value const& value, std::string_view name) -> std::expected<std::variant<std::bool_t, std::int64_t, std::string>, modulus::error> {
if (value.is_bool()) {
return std::variant<std::bool_t, std::int64_t, std::string>{value.as_bool()};
}
if (value.is_string()) {
return std::variant<std::bool_t, std::int64_t, std::string>{
std::string{value.as_string()},
};
}
if (value.is_int64()) {
return std::variant<std::bool_t, std::int64_t, std::string>{value.as_int64()};
}
if (value.is_uint64()) {
if (value.as_uint64() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
return invalid_result<std::variant<std::bool_t, std::int64_t, std::string>>(std::string{name} + " pragma value is out of range");
}
return std::variant<std::bool_t, std::int64_t, std::string>{static_cast<std::int64_t>(value.as_uint64())};
}
return invalid_result<std::variant<std::bool_t, std::int64_t, std::string>>(std::string{name} + " pragma value must be a string, integer, or boolean");
}
auto parse_pragmas(boost::json::object const& object) -> std::expected<std::vector<noto::pragma_setting>, modulus::error> {
auto found = object.find("pragmas");
if (found == object.end()) {
return std::vector<noto::pragma_setting>{};
}
auto pragma_object = modulus::json_config::require_object(found->value(), "database pragmas", json_options);
if (!pragma_object) {
return noto::detail::fail<std::vector<noto::pragma_setting>>(pragma_object.error());
}
auto pragmas = std::vector<noto::pragma_setting>{};
for (auto const& item : **pragma_object) {
auto name = std::string{item.key()};
if (!valid_pragma_name(name)) {
return invalid_result<std::vector<noto::pragma_setting>>("database pragma names must contain only letters, digits, or underscores");
}
auto value = parse_pragma_value(item.value(), name);
if (!value) {
return noto::detail::fail<std::vector<noto::pragma_setting>>(value.error());
}
pragmas.push_back({
.name = std::move(name),
.value = std::move(*value),
});
}
return pragmas;
}
}
auto noto::validate(noto::connection_config const& config) -> std::expected<void, modulus::error> {
if (config.filename.empty()) {
return invalid("database filename must not be empty");
}
if (contains_null(config.filename)) {
return invalid("database filename must not contain null bytes");
}
if (contains_null(config.name)) {
return invalid("database name must not contain null bytes");
}
if (config.busy_timeout.count() <= 0) {
return invalid("database busy timeout must be positive");
}
if (static_cast<std::uint64_t>(config.busy_timeout.count()) > sqlite_int_max()) {
return invalid("database busy timeout is out of range");
}
for (auto const& pragma : config.pragmas) {
if (!valid_pragma_name(pragma.name)) {
return invalid("database pragma names must contain only letters, digits, or underscores");
}
}
return {};
}
auto noto::memory_connection_config(std::string name) -> noto::connection_config {
return {
.name = std::move(name),
.filename = ":memory:",
.mode = noto::open_mode::MEMORY,
};
}
auto noto::file_connection_config(std::string filename, std::string name) -> noto::connection_config {
return {
.name = std::move(name),
.filename = std::move(filename),
};
}
auto noto::parse_database_config(boost::json::value const& value) -> std::expected<noto::connection_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "database", json_options);
if (!object) {
return noto::detail::fail<noto::connection_config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"name",
"filename",
"mode",
"uri",
"busy_timeout_ms",
"foreign_keys",
"pragmas",
}, "database", json_options);
if (!unknown) {
return noto::detail::fail<noto::connection_config>(unknown.error());
}
auto name = required_string_field(**object, "name");
if (!name) {
return noto::detail::fail<noto::connection_config>(name.error());
}
auto mode = modulus::json_config::optional_enum_field(**object, "mode", noto::open_mode::READ_WRITE_CREATE, {
{
.name = "read_only",
.value = noto::open_mode::READ_ONLY,
},
{
.name = "read_write",
.value = noto::open_mode::READ_WRITE,
},
{
.name = "read_write_create",
.value = noto::open_mode::READ_WRITE_CREATE,
},
{
.name = "memory",
.value = noto::open_mode::MEMORY,
},
}, "database mode must be read_only, read_write, read_write_create, or memory", json_options);
if (!mode) {
return noto::detail::fail<noto::connection_config>(mode.error());
}
auto filename = optional_string_field(**object, "filename");
if (!filename) {
return noto::detail::fail<noto::connection_config>(filename.error());
}
if (!*filename && *mode != noto::open_mode::MEMORY) {
return noto::detail::fail<noto::connection_config>(noto::detail::error_invalid_argument, "database filename is required");
}
auto uri = modulus::json_config::optional_bool_field(**object, "uri", false, json_options);
if (!uri) {
return noto::detail::fail<noto::connection_config>(uri.error());
}
auto foreign_keys = modulus::json_config::optional_bool_field(**object, "foreign_keys", true, json_options);
if (!foreign_keys) {
return noto::detail::fail<noto::connection_config>(foreign_keys.error());
}
auto timeout = modulus::json_config::positive_milliseconds_field(**object, "busy_timeout_ms", std::chrono::milliseconds{5000}, sqlite_int_max(), json_options);
if (!timeout) {
return noto::detail::fail<noto::connection_config>(timeout.error());
}
auto pragmas = parse_pragmas(**object);
if (!pragmas) {
return noto::detail::fail<noto::connection_config>(pragmas.error());
}
auto config = noto::connection_config{
.name = std::move(*name),
.filename = *filename ? std::move(**filename) : std::string{":memory:"},
.mode = *mode,
.uri = *uri,
.busy_timeout = *timeout,
.foreign_keys = *foreign_keys,
.pragmas = std::move(*pragmas),
};
auto valid = noto::validate(config);
if (!valid) {
return std::unexpected(valid.error());
}
return config;
}
auto noto::parse_database_configs(boost::json::value const& value) -> std::expected<std::vector<noto::connection_config>, modulus::error> {
auto object = modulus::json_config::require_object(value, "config root", json_options);
if (!object) {
return noto::detail::fail<std::vector<noto::connection_config>>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {"databases"}, "config root", json_options);
if (!unknown) {
return noto::detail::fail<std::vector<noto::connection_config>>(unknown.error());
}
auto found = (*object)->find("databases");
if (found == (*object)->end() || !found->value().is_array()) {
return invalid_result<std::vector<noto::connection_config>>("config databases array is required");
}
auto configs = std::vector<noto::connection_config>{};
for (auto const& database_value : found->value().as_array()) {
auto parsed = noto::parse_database_config(database_value);
if (!parsed) {
return noto::detail::fail<std::vector<noto::connection_config>>(parsed.error());
}
configs.push_back(std::move(*parsed));
}
return configs;
}
auto noto::load_database_configs(std::string const& path) -> std::expected<std::vector<noto::connection_config>, modulus::error> {
auto value = modulus::json_config::load_value(path, json_options);
if (!value) {
return noto::detail::fail<std::vector<noto::connection_config>>(value.error());
}
return noto::parse_database_configs(*value);
}
auto noto::to_json(std::variant<std::bool_t, std::int64_t, std::string> const& value) -> boost::json::value {
return std::visit([](auto const& item) -> boost::json::value {
return boost::json::value{item};
}, value);
}
auto noto::to_json(noto::connection_config const& config) -> boost::json::value {
auto pragmas = boost::json::object{};
for (auto const& pragma : config.pragmas) {
pragmas[pragma.name] = noto::to_json(pragma.value);
}
return boost::json::object{
{
"name",
config.name,
},
{
"filename",
config.filename,
},
{
"mode",
std::string{mode_name(config.mode)},
},
{
"uri",
config.uri,
},
{
"busy_timeout_ms",
config.busy_timeout.count(),
},
{
"foreign_keys",
config.foreign_keys,
},
{
"pragmas",
std::move(pragmas),
},
};
}
auto noto::to_json(std::vector<noto::connection_config> const& configs) -> boost::json::value {
auto databases = boost::json::array{};
for (auto const& config : configs) {
databases.push_back(noto::to_json(config));
}
return boost::json::object{
{
"databases",
std::move(databases),
},
};
}
+593
View File
@@ -0,0 +1,593 @@
#include <noto/connection.hpp>
#include <cassert>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include <boost/asio/awaitable.hpp>
#include <modulus.hpp>
#include <modulus/log.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 {
auto invalid_connection() -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "database connection is not valid");
}
auto invalid_statement() -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "database statement is not valid");
}
auto invalid(std::string message) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message));
}
auto async_status(std::expected<void, modulus::error> status) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return status;
}
auto contains_null(std::string_view value) -> std::bool_t {
return value.find('\0') != std::string_view::npos;
}
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 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 first_int(noto::query_result const& result) -> std::expected<std::int64_t, modulus::error> {
if (result.rows.empty() || result.rows.front().empty()) {
return noto::detail::fail<std::int64_t>(noto::detail::error_not_found, "query returned no rows");
}
auto const& value = result.rows.front().front().value;
if (auto integer = std::get_if<std::int64_t>(&value)) {
return *integer;
}
return noto::detail::fail<std::int64_t>(noto::detail::error_invalid_argument, "query did not return an integer");
}
auto first_string(noto::query_result const& result) -> std::expected<std::string, modulus::error> {
if (result.rows.empty() || result.rows.front().empty()) {
return noto::detail::fail<std::string>(noto::detail::error_not_found, "query returned no rows");
}
auto const& value = result.rows.front().front().value;
if (auto text = std::get_if<std::string>(&value)) {
return *text;
}
if (auto integer = std::get_if<std::int64_t>(&value)) {
return std::to_string(*integer);
}
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, "query did not return text");
}
auto async_statement_reset(std::shared_ptr<noto::detail::statement_core> core, std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_statement();
}
co_return co_await core->async_reset(std::move(params));
}
auto async_statement_step(std::shared_ptr<noto::detail::statement_core> core) -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_statement().error());
}
co_return co_await core->async_step();
}
auto async_statement_execute(std::shared_ptr<noto::detail::statement_core> core, std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_statement().error());
}
co_return co_await core->async_execute(std::move(params), std::move(options));
}
auto async_statement_finalize(std::shared_ptr<noto::detail::statement_core> core) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_statement();
}
co_return co_await core->async_finalize();
}
auto async_connection_connect(std::shared_ptr<noto::detail::connection_core> core) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_connect();
}
auto async_connection_close(std::shared_ptr<noto::detail::connection_core> core) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_close();
}
auto async_connection_execute(std::shared_ptr<noto::detail::connection_core> core, std::string sql, std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_connection().error());
}
co_return co_await core->async_execute(std::move(sql), std::move(params), std::move(options));
}
auto async_connection_prepare(std::shared_ptr<noto::detail::connection_core> core, std::string sql) -> boost::asio::awaitable<std::expected<noto::statement, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_connection().error());
}
co_return co_await core->async_prepare(std::move(sql));
}
auto async_connection_begin_transaction(std::shared_ptr<noto::detail::connection_core> core, noto::transaction_mode mode) -> boost::asio::awaitable<std::expected<noto::transaction, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_connection().error());
}
co_return co_await core->async_begin_transaction(mode);
}
auto async_connection_backup_to_file(std::shared_ptr<noto::detail::connection_core> core, std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_backup_to_file(std::move(path), std::move(options));
}
auto async_connection_restore_from_file(std::shared_ptr<noto::detail::connection_core> core, std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_restore_from_file(std::move(path), std::move(options));
}
auto async_connection_export_sql(std::shared_ptr<noto::detail::connection_core> core, noto::export_options options) -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
if (!core) {
co_return std::unexpected(invalid_connection().error());
}
co_return co_await core->async_export_sql(std::move(options));
}
auto async_connection_export_sql_to_file(std::shared_ptr<noto::detail::connection_core> core, std::string path, noto::export_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_export_sql_to_file(std::move(path), std::move(options));
}
auto async_connection_create_table(noto::connection connection, noto::table_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = noto::create_table_sql(definition);
if (!sql) {
co_return std::unexpected(sql.error());
}
auto result = co_await connection.async_execute(*sql);
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto async_connection_drop_table(noto::connection connection, noto::table_name table, std::bool_t if_exists) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = noto::drop_table_sql(table, if_exists);
if (!sql) {
co_return std::unexpected(sql.error());
}
auto result = co_await connection.async_execute(*sql);
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto async_connection_create_index(noto::connection connection, noto::index_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = noto::create_index_sql(definition);
if (!sql) {
co_return std::unexpected(sql.error());
}
auto result = co_await connection.async_execute(*sql);
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto async_connection_drop_index(noto::connection connection, noto::index_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = noto::drop_index_sql(definition);
if (!sql) {
co_return std::unexpected(sql.error());
}
auto result = co_await connection.async_execute(*sql);
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto async_connection_integrity_check(noto::connection connection) -> boost::asio::awaitable<std::expected<std::vector<std::string>, modulus::error>> {
auto result = co_await connection.async_execute("PRAGMA integrity_check");
if (!result) {
co_return std::unexpected(result.error());
}
auto messages = std::vector<std::string>{};
for (auto const& row : result->rows) {
if (!row.empty()) {
if (auto text = std::get_if<std::string>(&row.front().value)) {
messages.push_back(*text);
}
}
}
co_return messages;
}
auto async_connection_quick_check(noto::connection connection) -> boost::asio::awaitable<std::expected<std::vector<std::string>, modulus::error>> {
auto result = co_await connection.async_execute("PRAGMA quick_check");
if (!result) {
co_return std::unexpected(result.error());
}
auto messages = std::vector<std::string>{};
for (auto const& row : result->rows) {
if (!row.empty()) {
if (auto text = std::get_if<std::string>(&row.front().value)) {
messages.push_back(*text);
}
}
}
co_return messages;
}
auto async_connection_foreign_key_check(noto::connection connection) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
co_return co_await connection.async_execute("PRAGMA foreign_key_check");
}
auto async_connection_get_pragma(noto::connection connection, std::string name) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
if (!valid_token(name)) {
co_return std::unexpected(invalid("database pragma names must contain only letters, digits, or underscores").error());
}
co_return co_await connection.async_execute("PRAGMA " + name);
}
auto async_connection_set_pragma(noto::connection connection, std::string name, std::variant<std::bool_t, std::int64_t, std::string> value) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!valid_token(name)) {
co_return invalid("database pragma names must contain only letters, digits, or underscores");
}
auto rendered = pragma_value_sql(value);
if (!rendered) {
co_return std::unexpected(rendered.error());
}
auto result = co_await connection.async_execute("PRAGMA " + name + " = " + *rendered);
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto async_connection_foreign_keys(noto::connection connection) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
auto result = co_await async_connection_get_pragma(std::move(connection), "foreign_keys");
if (!result) {
co_return std::unexpected(result.error());
}
auto integer = first_int(*result);
if (!integer) {
co_return std::unexpected(integer.error());
}
co_return *integer != 0;
}
auto async_connection_set_foreign_keys(noto::connection connection, std::bool_t enabled) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await async_connection_set_pragma(std::move(connection), "foreign_keys", enabled);
}
auto async_connection_busy_timeout(noto::connection connection) -> boost::asio::awaitable<std::expected<std::chrono::milliseconds, modulus::error>> {
auto result = co_await async_connection_get_pragma(std::move(connection), "busy_timeout");
if (!result) {
co_return std::unexpected(result.error());
}
auto integer = first_int(*result);
if (!integer) {
co_return std::unexpected(integer.error());
}
co_return std::chrono::milliseconds{*integer};
}
auto async_connection_set_busy_timeout(std::shared_ptr<noto::detail::connection_core> core, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!core) {
co_return invalid_connection();
}
co_return co_await core->async_set_busy_timeout(timeout);
}
auto async_connection_user_version(noto::connection connection) -> boost::asio::awaitable<std::expected<std::int64_t, modulus::error>> {
auto result = co_await async_connection_get_pragma(std::move(connection), "user_version");
if (!result) {
co_return std::unexpected(result.error());
}
co_return first_int(*result);
}
auto async_connection_set_user_version(noto::connection connection, std::int64_t version) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await async_connection_set_pragma(std::move(connection), "user_version", version);
}
auto async_connection_application_id(noto::connection connection) -> boost::asio::awaitable<std::expected<std::int64_t, modulus::error>> {
auto result = co_await async_connection_get_pragma(std::move(connection), "application_id");
if (!result) {
co_return std::unexpected(result.error());
}
co_return first_int(*result);
}
auto async_connection_set_application_id(noto::connection connection, std::int64_t id) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await async_connection_set_pragma(std::move(connection), "application_id", id);
}
auto async_connection_journal_mode(noto::connection connection, std::string mode) -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
if (!valid_token(mode)) {
co_return std::unexpected(invalid("journal mode must contain only letters, digits, or underscores").error());
}
auto result = co_await connection.async_execute("PRAGMA journal_mode = " + mode);
if (!result) {
co_return std::unexpected(result.error());
}
co_return first_string(*result);
}
auto async_connection_wal_checkpoint(noto::connection connection, std::string mode) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
if (!valid_token(mode)) {
co_return std::unexpected(invalid("WAL checkpoint mode must contain only letters, digits, or underscores").error());
}
co_return co_await connection.async_execute("PRAGMA wal_checkpoint(" + mode + ")");
}
}
noto::sqlite_value::sqlite_value(std::nullptr_t) : value{std::monostate{}} {}
noto::sqlite_value::sqlite_value(std::int32_t value) : value{static_cast<std::int64_t>(value)} {}
noto::sqlite_value::sqlite_value(std::int64_t current) : value{current} {}
noto::sqlite_value::sqlite_value(std::float64_t current) : value{current} {}
noto::sqlite_value::sqlite_value(std::char_t const* current) : value{std::string{current == nullptr ? "" : current}} {}
noto::sqlite_value::sqlite_value(std::string current) : value{std::move(current)} {}
noto::sqlite_value::sqlite_value(std::string_view current) : value{std::string{current}} {}
noto::sqlite_value::sqlite_value(std::vector<std::byte> current) : value{std::move(current)} {}
auto noto::sqlite_value::is_null() const -> std::bool_t {
return std::holds_alternative<std::monostate>(value);
}
noto::statement::~statement() = default;
auto noto::statement::valid() const -> std::bool_t {
return core_ && !core_->finalized.load(std::memory_order_acquire);
}
auto noto::statement::async_reset(std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_statement_reset(core_, std::move(params));
}
auto noto::statement::async_step() -> boost::asio::awaitable<std::expected<std::optional<std::vector<noto::sqlite_value>>, modulus::error>> {
return async_statement_step(core_);
}
auto noto::statement::async_execute(std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_execute(std::move(params), noto::execute_options{});
}
auto noto::statement::async_execute(std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_statement_execute(core_, std::move(params), std::move(options));
}
auto noto::statement::async_finalize() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto core = core_;
if (!core) {
return async_status(invalid_statement());
}
return async_statement_finalize(std::move(core));
}
noto::statement::statement(std::shared_ptr<noto::detail::statement_core> core) : core_{std::move(core)} {}
noto::connection::connection(boost::asio::any_io_executor executor, noto::connection_config config) : core_{std::make_shared<noto::detail::connection_core>(std::move(executor), std::move(config))} {}
noto::connection::~connection() = default;
auto noto::connection::config() const -> noto::connection_config {
return core_ ? core_->config : noto::connection_config{};
}
auto noto::connection::state() const -> noto::connection_state {
return core_ ? core_->current_state() : noto::connection_state::CLOSED;
}
auto noto::connection::async_connect() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
assert(core_ != nullptr);
modulus::log_debug("noto connection opening");
return async_connection_connect(core_);
}
auto noto::connection::async_close() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
assert(core_ != nullptr);
modulus::log_debug("noto connection closing");
return async_connection_close(core_);
}
auto noto::connection::async_execute(std::string sql, std::vector<noto::sqlite_value> params) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_execute(std::move(sql), std::move(params), noto::execute_options{});
}
auto noto::connection::async_execute(std::string sql, std::vector<noto::sqlite_value> params, noto::execute_options options) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
assert(core_ != nullptr);
modulus::log_trace("noto executing SQL statement");
return async_connection_execute(core_, std::move(sql), std::move(params), std::move(options));
}
auto noto::connection::async_prepare(std::string sql) -> boost::asio::awaitable<std::expected<noto::statement, modulus::error>> {
return async_connection_prepare(core_, std::move(sql));
}
auto noto::connection::async_begin_transaction(noto::transaction_mode mode) -> boost::asio::awaitable<std::expected<noto::transaction, modulus::error>> {
assert(core_ != nullptr);
modulus::log_debug("noto transaction begin requested");
return async_connection_begin_transaction(core_, mode);
}
auto noto::connection::async_create_table(noto::table_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_create_table(noto::connection{core_}, std::move(definition));
}
auto noto::connection::async_drop_table(noto::table_name table, std::bool_t if_exists) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_drop_table(noto::connection{core_}, std::move(table), if_exists);
}
auto noto::connection::async_create_index(noto::index_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_create_index(noto::connection{core_}, std::move(definition));
}
auto noto::connection::async_drop_index(noto::index_definition definition) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_drop_index(noto::connection{core_}, std::move(definition));
}
auto noto::connection::async_backup_to_file(std::string path) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto options = noto::backup_options{};
return async_backup_to_file(std::move(path), std::move(options));
}
auto noto::connection::async_backup_to_file(std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_backup_to_file(core_, std::move(path), std::move(options));
}
auto noto::connection::async_restore_from_file(std::string path) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto options = noto::backup_options{};
return async_restore_from_file(std::move(path), std::move(options));
}
auto noto::connection::async_restore_from_file(std::string path, noto::backup_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_restore_from_file(core_, std::move(path), std::move(options));
}
auto noto::connection::async_export_sql() -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
return async_export_sql(noto::export_options{});
}
auto noto::connection::async_export_sql(noto::export_options options) -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
return async_connection_export_sql(core_, std::move(options));
}
auto noto::connection::async_export_sql_to_file(std::string path) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto options = noto::export_options{};
return async_export_sql_to_file(std::move(path), std::move(options));
}
auto noto::connection::async_export_sql_to_file(std::string path, noto::export_options options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_export_sql_to_file(core_, std::move(path), std::move(options));
}
auto noto::connection::async_integrity_check() -> boost::asio::awaitable<std::expected<std::vector<std::string>, modulus::error>> {
return async_connection_integrity_check(noto::connection{core_});
}
auto noto::connection::async_quick_check() -> boost::asio::awaitable<std::expected<std::vector<std::string>, modulus::error>> {
return async_connection_quick_check(noto::connection{core_});
}
auto noto::connection::async_foreign_key_check() -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_connection_foreign_key_check(noto::connection{core_});
}
auto noto::connection::async_get_pragma(std::string name) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_connection_get_pragma(noto::connection{core_}, std::move(name));
}
auto noto::connection::async_set_pragma(std::string name, std::variant<std::bool_t, std::int64_t, std::string> value) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (name == "busy_timeout") {
if (auto timeout = std::get_if<std::int64_t>(&value)) {
return async_connection_set_busy_timeout(core_, std::chrono::milliseconds{*timeout});
}
}
return async_connection_set_pragma(noto::connection{core_}, std::move(name), std::move(value));
}
auto noto::connection::async_foreign_keys() -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
return async_connection_foreign_keys(noto::connection{core_});
}
auto noto::connection::async_set_foreign_keys(std::bool_t enabled) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_set_foreign_keys(noto::connection{core_}, enabled);
}
auto noto::connection::async_busy_timeout() -> boost::asio::awaitable<std::expected<std::chrono::milliseconds, modulus::error>> {
return async_connection_busy_timeout(noto::connection{core_});
}
auto noto::connection::async_set_busy_timeout(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_set_busy_timeout(core_, timeout);
}
auto noto::connection::async_user_version() -> boost::asio::awaitable<std::expected<std::int64_t, modulus::error>> {
return async_connection_user_version(noto::connection{core_});
}
auto noto::connection::async_set_user_version(std::int64_t version) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_set_user_version(noto::connection{core_}, version);
}
auto noto::connection::async_application_id() -> boost::asio::awaitable<std::expected<std::int64_t, modulus::error>> {
return async_connection_application_id(noto::connection{core_});
}
auto noto::connection::async_set_application_id(std::int64_t id) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_connection_set_application_id(noto::connection{core_}, id);
}
auto noto::connection::async_journal_mode(std::string mode) -> boost::asio::awaitable<std::expected<std::string, modulus::error>> {
return async_connection_journal_mode(noto::connection{core_}, std::move(mode));
}
auto noto::connection::async_wal_checkpoint(std::string mode) -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
return async_connection_wal_checkpoint(noto::connection{core_}, std::move(mode));
}
noto::connection::connection(std::shared_ptr<noto::detail::connection_core> core) : core_{std::move(core)} {}
auto noto::connection::core() const -> std::shared_ptr<noto::detail::connection_core> {
return core_;
}
+2024
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
#include <noto/detail/errors.hpp>
#include <cassert>
#include <cstdint>
#include <expected>
#include <string>
#include <utility>
#include <modulus.hpp>
auto noto::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
return modulus::make_error("noto", code, std::move(message), location);
}
auto noto::detail::fail_void(modulus::error error) -> std::expected<void, modulus::error> {
return std::unexpected(std::move(error));
}
auto noto::detail::fail_void(std::int32_t code, std::string message, modulus::source_location location) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::make_error(code, std::move(message), location));
}
auto noto::detail::fail_void(std::expected<void, modulus::error> const& result) -> std::expected<void, modulus::error> {
assert(!result);
return noto::detail::fail_void(result.error());
}
+32
View File
@@ -0,0 +1,32 @@
#include <noto/detail/sql.hpp>
#include <expected>
#include <string>
#include <string_view>
#include <utility>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
auto noto::detail::quote_sql_literal(std::string_view value, std::string_view label, std::bool_t allow_empty) -> std::expected<std::string, modulus::error> {
if (value.empty() && !allow_empty) {
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, std::string{label} + " must not be empty");
}
auto quoted = std::string{"'"};
quoted.reserve((value.size() * 2U) + 2U);
for (auto character : value) {
switch (character) {
case '\0':
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, std::string{label} + " must not contain null bytes");
case '\'':
quoted += "''";
break;
default:
quoted += character;
break;
}
}
quoted += "'";
return modulus::success(std::move(quoted));
}
+79
View File
@@ -0,0 +1,79 @@
#include <noto/errors.hpp>
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
#include <sqlite3.h>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
namespace {
auto code_from(std::int32_t code) -> std::int32_t {
switch (code & 0xff) {
case SQLITE_OK:
case SQLITE_ROW:
case SQLITE_DONE:
return 0;
case SQLITE_MISUSE:
case SQLITE_RANGE:
return noto::detail::error_invalid_argument;
case SQLITE_NOTFOUND:
case SQLITE_EMPTY:
return noto::detail::error_not_found;
case SQLITE_BUSY:
case SQLITE_LOCKED:
return noto::detail::error_unavailable;
case SQLITE_INTERRUPT:
return noto::detail::error_cancelled;
case SQLITE_AUTH:
case SQLITE_PERM:
case SQLITE_READONLY:
return noto::detail::error_permission_denied;
case SQLITE_CONSTRAINT:
return noto::detail::error_constraint_violation;
case SQLITE_ERROR:
return noto::detail::error_syntax_error;
case SQLITE_CORRUPT:
case SQLITE_NOTADB:
return noto::detail::error_data_loss;
case SQLITE_IOERR:
case SQLITE_CANTOPEN:
return noto::detail::error_unavailable;
case SQLITE_NOMEM:
return noto::detail::error_resource_exhausted;
default:
return noto::detail::error_internal;
}
}
auto message_from(std::int32_t code, sqlite3* database, std::string_view operation) -> std::string {
auto message = std::string{};
if (!operation.empty()) {
message += operation;
message += ": ";
}
if (database != nullptr) {
message += sqlite3_errmsg(database);
} else {
message += sqlite3_errstr(code);
}
return message;
}
}
auto noto::error_from(std::int32_t code, sqlite3* database, std::string_view operation) -> modulus::error {
return noto::detail::make_error(code_from(code), message_from(code, database, operation));
}
auto noto::result_from(std::int32_t code, sqlite3* database, std::string_view operation) -> std::expected<void, modulus::error> {
if (code == SQLITE_OK || code == SQLITE_ROW || code == SQLITE_DONE) {
return {};
}
return noto::detail::fail_void(noto::error_from(code, database, operation));
}
+322
View File
@@ -0,0 +1,322 @@
#include <noto/migration.hpp>
#include <algorithm>
#include <cassert>
#include <expected>
#include <functional>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include <boost/asio/awaitable.hpp>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
#include <noto/detail/sql.hpp>
#include <noto/connection.hpp>
#include <noto/schema.hpp>
#include <noto/transaction.hpp>
#include <cstdint>
namespace {
auto invalid(std::string message) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::move(message));
}
auto internal(std::string message) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_internal, std::move(message));
}
auto contains_null(std::string_view value) -> std::bool_t {
return value.find('\0') != std::string_view::npos;
}
auto qualified_table(noto::table_name const& table) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(table.name);
if (!name) {
return std::unexpected(name.error());
}
if (table.database.empty()) {
return *name;
}
auto database = noto::quote_identifier(table.database);
if (!database) {
return std::unexpected(database.error());
}
return *database + "." + *name;
}
auto migration_table_sql(noto::migration_options const& options) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table);
if (!table) {
return std::unexpected(table.error());
}
return "CREATE TABLE IF NOT EXISTS " + *table + " (\"id\" TEXT NOT NULL PRIMARY KEY, " "\"description\" TEXT NOT NULL, " "\"checksum\" TEXT NOT NULL, " "\"applied_at\" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) STRICT";
}
auto select_records_sql(noto::migration_options const& options) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table);
if (!table) {
return std::unexpected(table.error());
}
return "SELECT \"id\", \"description\", \"checksum\", \"applied_at\" FROM " + *table + " ORDER BY \"id\"";
}
auto insert_record_sql(noto::migration_options const& options, noto::migration const& migration) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table);
if (!table) {
return std::unexpected(table.error());
}
auto id = noto::detail::quote_sql_literal(migration.id, "migration id");
if (!id) {
return std::unexpected(id.error());
}
auto description = noto::detail::quote_sql_literal(migration.description, "migration description");
if (!description) {
return std::unexpected(description.error());
}
auto checksum = noto::detail::quote_sql_literal(migration.checksum, "migration checksum");
if (!checksum) {
return std::unexpected(checksum.error());
}
return "INSERT INTO " + *table + " (\"id\", \"description\", \"checksum\") VALUES (" + *id + ", " + *description + ", " + *checksum + ")";
}
auto delete_record_sql(noto::migration_options const& options, std::string_view id_value) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(options.table);
if (!table) {
return std::unexpected(table.error());
}
auto id = noto::detail::quote_sql_literal(id_value, "migration id");
if (!id) {
return std::unexpected(id.error());
}
return "DELETE FROM " + *table + " WHERE \"id\" = " + *id;
}
auto validate_and_sort(std::vector<noto::migration> migrations, std::bool_t require_up) -> std::expected<std::vector<noto::migration>, modulus::error> {
auto ids = std::set<std::string>{};
for (auto const& migration : migrations) {
if (migration.id.empty()) {
return std::unexpected(invalid("migration id must not be empty").error());
}
if (contains_null(migration.id)) {
return std::unexpected(invalid("migration id must not contain null bytes").error());
}
if (!ids.insert(migration.id).second) {
return std::unexpected(invalid("migration ids must be unique").error());
}
if (require_up && !migration.up) {
return std::unexpected(invalid("migration up callback must be set").error());
}
if (contains_null(migration.description)) {
return std::unexpected(invalid("migration description must not contain null bytes").error());
}
if (contains_null(migration.checksum)) {
return std::unexpected(invalid("migration checksum must not contain null bytes").error());
}
}
std::ranges::sort(migrations, {}, &noto::migration::id);
return migrations;
}
auto as_string(noto::sqlite_value const& value) -> std::string {
if (auto text = std::get_if<std::string>(&value.value)) {
return *text;
}
if (auto integer = std::get_if<std::int64_t>(&value.value)) {
return std::to_string(*integer);
}
return {};
}
auto execute_status(noto::connection& connection, std::string sql) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto result = co_await connection.async_execute(std::move(sql));
if (!result) {
co_return std::unexpected(result.error());
}
co_return std::expected<void, modulus::error>{};
}
auto ensure_migration_table(noto::connection& connection, noto::migration_options const& options) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto sql = migration_table_sql(options);
if (!sql) {
co_return std::unexpected(sql.error());
}
co_return co_await execute_status(connection, *sql);
}
auto load_records(noto::connection& connection, noto::migration_options const& options) -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> {
auto sql = select_records_sql(options);
if (!sql) {
co_return std::unexpected(sql.error());
}
auto result = co_await connection.async_execute(*sql);
if (!result) {
co_return std::unexpected(result.error());
}
auto records = std::vector<noto::migration_record>{};
records.reserve(result->rows.size());
for (auto const& row : result->rows) {
if (row.size() < 4) {
co_return std::unexpected(internal("migration table returned an incomplete row").error());
}
records.push_back({
.id = as_string(row.at(0)),
.description = as_string(row.at(1)),
.checksum = as_string(row.at(2)),
.applied_at = as_string(row.at(3)),
});
}
co_return records;
}
auto applied_record(std::vector<noto::migration_record> const& records, std::string const& id) -> noto::migration_record const* {
for (auto const& record : records) {
if (record.id == id) {
return &record;
}
}
return nullptr;
}
auto rollback_and_return(noto::transaction& transaction, std::expected<void, modulus::error> status) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto rollback = co_await transaction.async_rollback();
if (!rollback) {
co_return rollback;
}
co_return status;
}
auto rollback_and_return(noto::transaction& transaction, modulus::error error) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return co_await rollback_and_return(transaction, noto::detail::fail_void(std::move(error)));
}
}
noto::migrator::migrator(noto::connection& connection, noto::migration_options options) : connection_{&connection}, options_{std::move(options)} {}
auto noto::migrator::async_applied_migrations() -> boost::asio::awaitable<std::expected<std::vector<noto::migration_record>, modulus::error>> {
if (connection_ == nullptr) {
co_return std::unexpected(invalid("migrator connection is not valid").error());
}
auto ensured = co_await ensure_migration_table(*connection_, options_);
if (!ensured) {
co_return std::unexpected(ensured.error());
}
co_return co_await load_records(*connection_, options_);
}
auto noto::migrator::async_migrate(std::vector<noto::migration> migrations) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
modulus::log_debug("noto migration run requested for " + std::to_string(migrations.size()) + " migrations");
if (connection_ == nullptr) {
co_return invalid("migrator connection is not valid");
}
auto sorted = validate_and_sort(std::move(migrations), true);
if (!sorted) {
co_return std::unexpected(sorted.error());
}
auto ensured = co_await ensure_migration_table(*connection_, options_);
if (!ensured) {
co_return ensured;
}
auto transaction_result = co_await connection_->async_begin_transaction(noto::transaction_mode::IMMEDIATE);
if (!transaction_result) {
co_return std::unexpected(transaction_result.error());
}
auto transaction = std::move(*transaction_result);
auto records = co_await load_records(*connection_, options_);
if (!records) {
co_return co_await rollback_and_return(transaction, records.error());
}
for (auto const& migration : *sorted) {
auto const* applied = applied_record(*records, migration.id);
if (applied != nullptr) {
if (applied->checksum != migration.checksum) {
co_return co_await rollback_and_return(transaction, invalid("applied migration checksum mismatch: " + migration.id));
}
modulus::log_trace("noto migration already applied: " + migration.id);
continue;
}
assert(static_cast<std::bool_t>(migration.up));
modulus::log_debug("noto applying migration: " + migration.id);
auto migrated = co_await migration.up(*connection_);
if (!migrated) {
co_return co_await rollback_and_return(transaction, migrated);
}
auto insert_sql = insert_record_sql(options_, migration);
if (!insert_sql) {
co_return co_await rollback_and_return(transaction, insert_sql.error());
}
auto inserted = co_await execute_status(*connection_, *insert_sql);
if (!inserted) {
co_return co_await rollback_and_return(transaction, inserted);
}
records->push_back({
.id = migration.id,
.description = migration.description,
.checksum = migration.checksum,
});
}
co_return co_await transaction.async_commit();
}
auto noto::migrator::async_rollback(std::vector<noto::migration> migrations, std::string target_id) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
modulus::log_debug("noto migration rollback requested");
if (connection_ == nullptr) {
co_return invalid("migrator connection is not valid");
}
auto sorted = validate_and_sort(std::move(migrations), false);
if (!sorted) {
co_return std::unexpected(sorted.error());
}
auto by_id = std::map<std::string, noto::migration>{};
for (auto& migration : *sorted) {
by_id.emplace(migration.id, std::move(migration));
}
auto ensured = co_await ensure_migration_table(*connection_, options_);
if (!ensured) {
co_return ensured;
}
auto transaction_result = co_await connection_->async_begin_transaction(noto::transaction_mode::IMMEDIATE);
if (!transaction_result) {
co_return std::unexpected(transaction_result.error());
}
auto transaction = std::move(*transaction_result);
auto records = co_await load_records(*connection_, options_);
if (!records) {
co_return co_await rollback_and_return(transaction, records.error());
}
std::ranges::sort(*records, std::greater<>{}, &noto::migration_record::id);
for (auto const& record : *records) {
if (!target_id.empty() && record.id <= target_id) {
continue;
}
auto found = by_id.find(record.id);
if (found == by_id.end()) {
co_return co_await rollback_and_return(transaction, invalid("applied migration is missing from rollback set: " + record.id));
}
if (!found->second.down) {
co_return co_await rollback_and_return(transaction, invalid("migration does not define a down callback: " + record.id));
}
auto reverted = co_await found->second.down(*connection_);
if (!reverted) {
co_return co_await rollback_and_return(transaction, reverted);
}
auto delete_sql = delete_record_sql(options_, record.id);
if (!delete_sql) {
co_return co_await rollback_and_return(transaction, delete_sql.error());
}
auto deleted = co_await execute_status(*connection_, *delete_sql);
if (!deleted) {
co_return co_await rollback_and_return(transaction, deleted);
}
}
co_return co_await transaction.async_commit();
}
+523
View File
@@ -0,0 +1,523 @@
#include <noto/schema.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <expected>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
#include <noto/detail/sql.hpp>
namespace {
auto invalid_sql(std::string message) -> std::expected<std::string, modulus::error> {
return noto::detail::fail<std::string>(noto::detail::error_invalid_argument, std::move(message));
}
template <typename VALUE>
auto invalid_value(std::string message) -> std::expected<VALUE, modulus::error> {
return noto::detail::fail<VALUE>(noto::detail::error_invalid_argument, std::move(message));
}
auto contains_null(std::string_view value) -> std::bool_t {
return std::ranges::find(value, '\0') != value.end();
}
auto validate_identifier(std::string_view identifier) -> std::expected<void, modulus::error> {
if (identifier.empty()) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "database identifier must not be empty");
}
if (contains_null(identifier)) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "database identifier must not contain null bytes");
}
return {};
}
auto validate_fragment(std::string_view value, std::string_view label) -> std::expected<void, modulus::error> {
if (value.empty()) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::string{label} + " must not be empty");
}
if (contains_null(value)) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, std::string{label} + " must not contain null bytes");
}
return {};
}
auto qualified_table(noto::table_name const& table) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(table.name);
if (!name) {
return std::unexpected(name.error());
}
if (table.database.empty()) {
return *name;
}
auto database = noto::quote_identifier(table.database);
if (!database) {
return std::unexpected(database.error());
}
return *database + "." + *name;
}
auto generated_storage_sql(noto::generated_column_storage storage) -> std::string_view {
switch (storage) {
case noto::generated_column_storage::VIRTUAL:
return "VIRTUAL";
case noto::generated_column_storage::STORED:
return "STORED";
}
return "VIRTUAL";
}
auto reference_action_sql(noto::reference_action action) -> std::string_view {
switch (action) {
case noto::reference_action::RESTRICT:
return "RESTRICT";
case noto::reference_action::CASCADE:
return "CASCADE";
case noto::reference_action::SET_NULL:
return "SET NULL";
case noto::reference_action::NO_ACTION:
return "NO ACTION";
case noto::reference_action::SET_DEFAULT:
return "SET DEFAULT";
}
return "RESTRICT";
}
auto append_identifier_list(std::ostringstream& sql, std::vector<std::string> const& identifiers) -> std::expected<void, modulus::error> {
if (identifiers.empty()) {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "column list must not be empty");
}
for (auto index = std::size_t{0}; index < identifiers.size(); ++index) {
auto identifier = noto::quote_identifier(identifiers[index]);
if (!identifier) {
return std::unexpected(identifier.error());
}
if (index != 0) {
sql << ", ";
}
sql << *identifier;
}
return {};
}
auto append_constraint_name(std::ostringstream& sql, std::string const& name) -> std::expected<void, modulus::error> {
if (name.empty()) {
return {};
}
auto quoted = noto::quote_identifier(name);
if (!quoted) {
return std::unexpected(quoted.error());
}
sql << "CONSTRAINT " << *quoted << " ";
return {};
}
auto column_sql(noto::column_definition const& column) -> std::expected<std::string, modulus::error> {
auto name = noto::quote_identifier(column.name);
if (!name) {
return std::unexpected(name.error());
}
auto type_status = validate_fragment(column.type.sql(), "column type");
if (!type_status) {
return std::unexpected(type_status.error());
}
if (column.auto_increment && (!column.primary_key || column.type.sql() != "INTEGER")) {
return invalid_sql("SQLite autoincrement columns must be INTEGER PRIMARY KEY columns");
}
auto sql = std::ostringstream{};
sql << *name << " " << column.type.sql();
if (column.generated) {
if (column.auto_increment) {
return invalid_sql("generated columns must not be auto increment columns");
}
if (column.default_value) {
return invalid_sql("generated columns must not define a default value");
}
auto expression_status = validate_fragment(column.generated->expression.value(), "generated column expression");
if (!expression_status) {
return std::unexpected(expression_status.error());
}
sql << " GENERATED ALWAYS AS (" << column.generated->expression.value() << ") " << generated_storage_sql(column.generated->storage);
}
if (column.auto_increment) {
sql << " PRIMARY KEY AUTOINCREMENT";
return sql.str();
}
if (!column.nullable) {
sql << " NOT NULL";
}
if (column.default_value) {
auto default_status = validate_fragment(column.default_value->sql(), "column default expression");
if (!default_status) {
return std::unexpected(default_status.error());
}
sql << " DEFAULT " << column.default_value->sql();
}
if (column.primary_key) {
sql << " PRIMARY KEY";
}
return sql.str();
}
auto primary_key_sql(std::vector<std::string> const& columns) -> std::expected<std::string, modulus::error> {
auto sql = std::ostringstream{};
sql << "PRIMARY KEY (";
auto status = append_identifier_list(sql, columns);
if (!status) {
return std::unexpected(status.error());
}
sql << ")";
return sql.str();
}
auto unique_constraint_sql(noto::unique_constraint_definition const& definition) -> std::expected<std::string, modulus::error> {
auto sql = std::ostringstream{};
auto name_status = append_constraint_name(sql, definition.name);
if (!name_status) {
return std::unexpected(name_status.error());
}
sql << "UNIQUE (";
auto columns = append_identifier_list(sql, definition.columns);
if (!columns) {
return std::unexpected(columns.error());
}
sql << ")";
return sql.str();
}
auto check_constraint_sql(noto::check_constraint_definition const& definition) -> std::expected<std::string, modulus::error> {
if (!definition.enforced) {
return invalid_sql("SQLite check constraints cannot be marked not enforced");
}
auto expression_status = validate_fragment(definition.expression.value(), "check constraint expression");
if (!expression_status) {
return std::unexpected(expression_status.error());
}
auto sql = std::ostringstream{};
auto name_status = append_constraint_name(sql, definition.name);
if (!name_status) {
return std::unexpected(name_status.error());
}
sql << "CHECK (" << definition.expression.value() << ")";
return sql.str();
}
auto foreign_key_sql(noto::foreign_key_definition const& definition) -> std::expected<std::string, modulus::error> {
auto target = qualified_table(definition.referenced_table);
if (!target) {
return std::unexpected(target.error());
}
auto sql = std::ostringstream{};
auto name_status = append_constraint_name(sql, definition.name);
if (!name_status) {
return std::unexpected(name_status.error());
}
sql << "FOREIGN KEY (";
auto columns = append_identifier_list(sql, definition.columns);
if (!columns) {
return std::unexpected(columns.error());
}
sql << ") REFERENCES " << *target << " (";
auto referenced_columns = append_identifier_list(sql, definition.referenced_columns);
if (!referenced_columns) {
return std::unexpected(referenced_columns.error());
}
sql << ")";
if (definition.on_delete) {
sql << " ON DELETE " << reference_action_sql(*definition.on_delete);
}
if (definition.on_update) {
sql << " ON UPDATE " << reference_action_sql(*definition.on_update);
}
return sql.str();
}
auto append_table_options(std::ostringstream& sql, noto::table_options const& options) -> void {
auto first = true;
auto append = [&sql, &first](std::string_view value) {
sql << (first ? " " : ", ") << value;
first = false;
};
if (options.strict) {
append("STRICT");
}
if (options.without_rowid) {
append("WITHOUT ROWID");
}
}
}
auto noto::trusted_sql_fragment::unsafe(std::string sql) -> std::expected<noto::trusted_sql_fragment, modulus::error> {
auto status = validate_fragment(sql, "trusted SQL fragment");
if (!status) {
return std::unexpected(status.error());
}
return noto::trusted_sql_fragment{std::move(sql)};
}
auto noto::trusted_sql_fragment::value() const -> std::string const& {
return sql_;
}
auto noto::trusted_sql_fragment::empty() const -> std::bool_t {
return sql_.empty();
}
noto::trusted_sql_fragment::trusted_sql_fragment(std::string sql) : sql_{std::move(sql)} {}
auto noto::column_type::integer() -> noto::column_type {
return noto::column_type{"INTEGER"};
}
auto noto::column_type::bigint() -> noto::column_type {
return noto::column_type{"INTEGER"};
}
auto noto::column_type::varchar(std::uint32_t length) -> std::expected<noto::column_type, modulus::error> {
if (length == 0) {
return invalid_value<noto::column_type>("VARCHAR length must be positive");
}
return noto::column_type{"TEXT"};
}
auto noto::column_type::text() -> noto::column_type {
return noto::column_type{"TEXT"};
}
auto noto::column_type::blob() -> noto::column_type {
return noto::column_type{"BLOB"};
}
auto noto::column_type::real() -> noto::column_type {
return noto::column_type{"REAL"};
}
auto noto::column_type::boolean() -> noto::column_type {
return noto::column_type{"INTEGER"};
}
auto noto::column_type::timestamp() -> noto::column_type {
return noto::column_type{"TEXT"};
}
auto noto::column_type::decimal(std::uint32_t precision, std::uint32_t scale) -> std::expected<noto::column_type, modulus::error> {
if (precision == 0) {
return invalid_value<noto::column_type>("DECIMAL precision must be positive");
}
if (scale > precision) {
return invalid_value<noto::column_type>("DECIMAL scale must be less than or equal to precision");
}
return noto::column_type{"NUMERIC"};
}
auto noto::column_type::trusted(noto::trusted_sql_fragment fragment) -> noto::column_type {
return noto::column_type{fragment.value()};
}
auto noto::column_type::sql() const -> std::string const& {
return sql_;
}
auto noto::column_type::empty() const -> std::bool_t {
return sql_.empty();
}
noto::column_type::column_type(std::string sql) : sql_{std::move(sql)} {}
auto noto::default_value::null_value() -> noto::default_value {
return noto::default_value{"NULL"};
}
auto noto::default_value::string(std::string_view value) -> std::expected<noto::default_value, modulus::error> {
auto literal = noto::detail::quote_sql_literal(value, "column default string");
if (!literal) {
return std::unexpected(literal.error());
}
return noto::default_value{*literal};
}
auto noto::default_value::integer(std::int64_t value) -> noto::default_value {
return noto::default_value{std::to_string(value)};
}
auto noto::default_value::unsigned_integer(std::uint64_t value) -> noto::default_value {
return noto::default_value{std::to_string(value)};
}
auto noto::default_value::real(std::float64_t value) -> std::expected<noto::default_value, modulus::error> {
if (!std::isfinite(value)) {
return invalid_value<noto::default_value>("column default real value must be finite");
}
auto sql = std::ostringstream{};
sql << value;
return noto::default_value{sql.str()};
}
auto noto::default_value::boolean(std::bool_t value) -> noto::default_value {
return noto::default_value{value ? "1" : "0"};
}
auto noto::default_value::current_timestamp() -> noto::default_value {
return noto::default_value{"CURRENT_TIMESTAMP"};
}
auto noto::default_value::trusted_expression(noto::trusted_sql_fragment fragment) -> noto::default_value {
return noto::default_value{fragment.value()};
}
auto noto::default_value::sql() const -> std::string const& {
return sql_;
}
auto noto::default_value::empty() const -> std::bool_t {
return sql_.empty();
}
noto::default_value::default_value(std::string sql) : sql_{std::move(sql)} {}
auto noto::quote_identifier(std::string_view identifier) -> std::expected<std::string, modulus::error> {
auto valid = validate_identifier(identifier);
if (!valid) {
return std::unexpected(valid.error());
}
auto quoted = std::string{"\""};
quoted.reserve(identifier.size() + 2);
for (std::char_t character : identifier) {
if (character == '"') {
quoted += "\"\"";
} else {
quoted += character;
}
}
quoted += "\"";
return quoted;
}
auto noto::create_table_sql(noto::table_definition const& definition) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(definition.name);
if (!table) {
return std::unexpected(table.error());
}
if (definition.columns.empty()) {
return invalid_sql("table must define at least one column");
}
auto inline_primary_key_count = std::ranges::count_if(definition.columns, [](noto::column_definition const& column) {
return column.primary_key;
});
if (!definition.primary_key.empty() && inline_primary_key_count != 0) {
return invalid_sql("table must not mix column primary keys with a table primary key");
}
if (definition.primary_key.empty() && inline_primary_key_count > 1) {
return invalid_sql("table must not define multiple column-level primary keys");
}
auto definitions = std::vector<std::string>{};
for (auto const& column_definition : definition.columns) {
auto column = column_sql(column_definition);
if (!column) {
return std::unexpected(column.error());
}
definitions.push_back(std::move(*column));
}
if (!definition.primary_key.empty()) {
auto constraint = primary_key_sql(definition.primary_key);
if (!constraint) {
return std::unexpected(constraint.error());
}
definitions.push_back(std::move(*constraint));
}
for (auto const& unique_constraint : definition.unique_constraints) {
auto constraint = unique_constraint_sql(unique_constraint);
if (!constraint) {
return std::unexpected(constraint.error());
}
definitions.push_back(std::move(*constraint));
}
for (auto const& check_constraint : definition.check_constraints) {
auto constraint = check_constraint_sql(check_constraint);
if (!constraint) {
return std::unexpected(constraint.error());
}
definitions.push_back(std::move(*constraint));
}
for (auto const& foreign_key : definition.foreign_keys) {
auto constraint = foreign_key_sql(foreign_key);
if (!constraint) {
return std::unexpected(constraint.error());
}
definitions.push_back(std::move(*constraint));
}
auto sql = std::ostringstream{};
sql << "CREATE TABLE " << (definition.if_not_exists ? "IF NOT EXISTS " : "") << *table << " (";
for (auto index = std::size_t{0}; index < definitions.size(); ++index) {
if (index != 0) {
sql << ", ";
}
sql << definitions[index];
}
sql << ")";
append_table_options(sql, definition.options);
return sql.str();
}
auto noto::drop_table_sql(noto::table_name const& table, std::bool_t if_exists) -> std::expected<std::string, modulus::error> {
auto name = qualified_table(table);
if (!name) {
return std::unexpected(name.error());
}
return std::string{"DROP TABLE "} + (if_exists ? "IF EXISTS " : "") + *name;
}
auto noto::create_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error> {
auto table = qualified_table(definition.table);
if (!table) {
return std::unexpected(table.error());
}
auto index = noto::quote_identifier(definition.name);
if (!index) {
return std::unexpected(index.error());
}
auto sql = std::ostringstream{};
sql << "CREATE " << (definition.unique ? "UNIQUE " : "") << "INDEX IF NOT EXISTS ";
if (!definition.table.database.empty()) {
auto database = noto::quote_identifier(definition.table.database);
if (!database) {
return std::unexpected(database.error());
}
sql << *database << ".";
}
sql << *index << " ON " << *table << " (";
auto columns = append_identifier_list(sql, definition.columns);
if (!columns) {
return std::unexpected(columns.error());
}
sql << ")";
return sql.str();
}
auto noto::drop_index_sql(noto::index_definition const& definition) -> std::expected<std::string, modulus::error> {
auto index = noto::quote_identifier(definition.name);
if (!index) {
return std::unexpected(index.error());
}
if (definition.table.database.empty()) {
return "DROP INDEX IF EXISTS " + *index;
}
auto database = noto::quote_identifier(definition.table.database);
if (!database) {
return std::unexpected(database.error());
}
return "DROP INDEX IF EXISTS " + *database + "." + *index;
}
auto noto::table_exists_sql() -> std::string {
return "SELECT \"name\" FROM sqlite_schema WHERE \"type\" = 'table' AND \"name\" = ?1";
}
+111
View File
@@ -0,0 +1,111 @@
#include <noto/transaction.hpp>
#include <atomic>
#include <expected>
#include <functional>
#include <memory>
#include <utility>
#include <boost/asio/awaitable.hpp>
#include <modulus.hpp>
#include <noto/detail/errors.hpp>
#include <noto/detail/core.hpp>
namespace {
auto inactive_transaction() -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_invalid_argument, "transaction is not active");
}
auto async_status(std::expected<void, modulus::error> status) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
co_return status;
}
auto finish_started_transaction(std::shared_ptr<noto::detail::transaction_state>& state, noto::detail::transaction_result const& result) -> void {
if (result.status) {
state->core.reset();
state->finish();
return;
}
if (result.active) {
if (state->detached.load(std::memory_order_acquire)) {
state->core->abandon_transaction();
state->core.reset();
state->finish();
} else {
state->active.store(true, std::memory_order_release);
}
return;
}
state->core.reset();
state->finish();
}
auto async_commit_started_impl(std::shared_ptr<noto::detail::transaction_state> state) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto result = co_await state->core->async_commit_transaction();
finish_started_transaction(state, result);
co_return result.status;
}
auto async_commit_impl(std::shared_ptr<noto::detail::transaction_state> state) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
modulus::log_debug("noto transaction commit requested");
if (!state || !state->core || !state->active.exchange(false, std::memory_order_acq_rel)) {
return async_status(inactive_transaction());
}
state->detached.store(false, std::memory_order_release);
return async_commit_started_impl(std::move(state));
}
auto async_rollback_started_impl(std::shared_ptr<noto::detail::transaction_state> state) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto result = co_await state->core->async_rollback_transaction();
finish_started_transaction(state, result);
co_return result.status;
}
auto async_rollback_impl(std::shared_ptr<noto::detail::transaction_state> state) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
modulus::log_debug("noto transaction rollback requested");
if (!state || !state->core || !state->active.exchange(false, std::memory_order_acq_rel)) {
return async_status(inactive_transaction());
}
state->detached.store(false, std::memory_order_release);
return async_rollback_started_impl(std::move(state));
}
}
noto::transaction::transaction(noto::transaction&& other) noexcept : state_{std::move(other.state_)} {}
noto::transaction::~transaction() {
if (!state_) {
return;
}
if (state_->active.exchange(false, std::memory_order_acq_rel) && state_->core) {
state_->core->abandon_transaction();
state_->finish();
return;
}
if (state_->core) {
state_->detached.store(true, std::memory_order_release);
return;
}
state_->finish();
}
auto noto::transaction::active() const -> std::bool_t {
return state_ && state_->active.load(std::memory_order_acquire);
}
auto noto::transaction::async_commit() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_commit_impl(state_);
}
auto noto::transaction::async_rollback() -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_rollback_impl(state_);
}
noto::transaction::transaction(std::shared_ptr<noto::detail::transaction_state> state) : state_{std::move(state)} {}
noto::transaction::transaction(std::shared_ptr<noto::detail::connection_core> core, std::function<void()> on_finish) : state_{std::make_shared<noto::detail::transaction_state>(std::move(core), std::move(on_finish))} {}