|
|
|
@@ -5,9 +5,6 @@
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <expected>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <iterator>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <utility>
|
|
|
|
@@ -26,38 +23,6 @@
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
constexpr auto application_id = std::int64_t{0x56414D50};
|
|
|
|
|
constexpr auto statement_delimiter = std::string_view{"-- vamp-statement"};
|
|
|
|
|
|
|
|
|
|
auto trim(std::string value) -> std::string {
|
|
|
|
|
auto first = value.find_first_not_of(" \t\r\n");
|
|
|
|
|
if (first == std::string::npos) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
auto last = value.find_last_not_of(" \t\r\n");
|
|
|
|
|
return value.substr(first, last - first + 1U);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto read_text(std::string const& path) -> std::expected<std::string, modulus::error> {
|
|
|
|
|
auto input = std::ifstream{path, std::ios::binary};
|
|
|
|
|
if (!input) {
|
|
|
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open migration: " + path));
|
|
|
|
|
}
|
|
|
|
|
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
|
|
|
|
|
if (!input.good() && !input.eof()) {
|
|
|
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to read migration: " + path));
|
|
|
|
|
}
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto execute_statements(noto::connection& connection, std::vector<std::string> statements) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
for (auto& statement : statements) {
|
|
|
|
|
auto result = co_await connection.async_execute(std::move(statement));
|
|
|
|
|
if (!result) {
|
|
|
|
|
co_return std::unexpected(result.error());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
co_return std::expected<void, modulus::error>{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto connection_config(vamp_zero::database_config const& config) -> noto::connection_config {
|
|
|
|
|
auto result = config.path == ":memory:" ? noto::memory_connection_config("product-vamp") : noto::file_connection_config(config.path, "product-vamp");
|
|
|
|
@@ -70,19 +35,165 @@ auto connection_config(vamp_zero::database_config const& config) -> noto::connec
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto read_migration(vamp_zero::database_config const& config, std::string filename) -> std::expected<std::vector<std::string>, modulus::error> {
|
|
|
|
|
auto migration_text = read_text(config.migrations_path + "/" + filename);
|
|
|
|
|
if (!migration_text) {
|
|
|
|
|
return std::unexpected(migration_text.error());
|
|
|
|
|
}
|
|
|
|
|
return vamp_zero::split_migration_statements(*migration_text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto text_value(noto::sqlite_value const& value) -> std::string {
|
|
|
|
|
auto text = std::get_if<std::string>(&value.value);
|
|
|
|
|
return text == nullptr ? std::string{} : *text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto execute_schema_statement(noto::connection& connection, std::string statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
auto result = co_await connection.async_execute(std::move(statement));
|
|
|
|
|
if (!result) {
|
|
|
|
|
co_return std::unexpected(result.error());
|
|
|
|
|
}
|
|
|
|
|
co_return std::expected<void, modulus::error>{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto column_exists(noto::connection& connection, std::string table, std::string column) -> boost::asio::awaitable<std::expected<std::bool_t, modulus::error>> {
|
|
|
|
|
auto selected = co_await connection.async_execute("PRAGMA table_info(" + table + ")");
|
|
|
|
|
if (!selected) {
|
|
|
|
|
co_return std::unexpected(selected.error());
|
|
|
|
|
}
|
|
|
|
|
for (auto const& row : selected->rows) {
|
|
|
|
|
if (row.size() >= 2U && text_value(row[1]) == column) {
|
|
|
|
|
co_return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
co_return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto ensure_column(noto::connection& connection, std::string table, std::string column, std::string alter_statement) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
auto exists = co_await column_exists(connection, std::move(table), std::move(column));
|
|
|
|
|
if (!exists) {
|
|
|
|
|
co_return std::unexpected(exists.error());
|
|
|
|
|
}
|
|
|
|
|
if (*exists) {
|
|
|
|
|
co_return std::expected<void, modulus::error>{};
|
|
|
|
|
}
|
|
|
|
|
co_return co_await execute_schema_statement(connection, std::move(alter_statement));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto ensure_current_schema(noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
auto begin = co_await connection.async_execute("BEGIN IMMEDIATE");
|
|
|
|
|
if (!begin) {
|
|
|
|
|
co_return std::unexpected(begin.error());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto ensured = std::expected<void, modulus::error>{};
|
|
|
|
|
auto schema_statements = std::vector<std::string>{
|
|
|
|
|
R"sql(
|
|
|
|
|
CREATE TABLE IF NOT EXISTS songs (
|
|
|
|
|
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 120),
|
|
|
|
|
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
|
|
|
|
|
composer TEXT NOT NULL DEFAULT '',
|
|
|
|
|
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
|
|
|
|
|
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
|
|
|
|
|
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms),
|
|
|
|
|
part TEXT NOT NULL DEFAULT '',
|
|
|
|
|
source_path TEXT NOT NULL DEFAULT '',
|
|
|
|
|
key_signature TEXT NOT NULL DEFAULT '',
|
|
|
|
|
catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))
|
|
|
|
|
) STRICT
|
|
|
|
|
)sql",
|
|
|
|
|
R"sql(
|
|
|
|
|
CREATE TABLE IF NOT EXISTS page_state (
|
|
|
|
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
|
|
|
active_song_id TEXT NOT NULL DEFAULT '',
|
|
|
|
|
page_index INTEGER NOT NULL DEFAULT 0 CHECK(page_index >= 0),
|
|
|
|
|
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= 0)
|
|
|
|
|
) STRICT
|
|
|
|
|
)sql",
|
|
|
|
|
R"sql(
|
|
|
|
|
CREATE TABLE IF NOT EXISTS practice_exercises (
|
|
|
|
|
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 160),
|
|
|
|
|
practice_type_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120),
|
|
|
|
|
practice_type_title TEXT NOT NULL DEFAULT 'General Practice' CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200),
|
|
|
|
|
lesson_group_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120),
|
|
|
|
|
lesson_group_title TEXT NOT NULL DEFAULT 'General' CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200),
|
|
|
|
|
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
|
|
|
|
|
composer TEXT NOT NULL DEFAULT '',
|
|
|
|
|
key_signature TEXT NOT NULL DEFAULT 'C',
|
|
|
|
|
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
|
|
|
|
|
source_path TEXT NOT NULL DEFAULT '',
|
|
|
|
|
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
|
|
|
|
|
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms),
|
|
|
|
|
catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))
|
|
|
|
|
) STRICT
|
|
|
|
|
)sql",
|
|
|
|
|
"DROP TABLE IF EXISTS _noto_schema_migrations",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (auto& statement : schema_statements) {
|
|
|
|
|
if (!ensured) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "songs", "part", "ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT ''");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "songs", "source_path", "ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "songs", "key_signature", "ALTER TABLE songs ADD COLUMN key_signature TEXT NOT NULL DEFAULT ''");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "songs", "catalog_managed", "ALTER TABLE songs ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_id", "ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(practice_type_id)) BETWEEN 1 AND 120)");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "practice_type_title", "ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice' CHECK(length(trim(practice_type_title)) BETWEEN 1 AND 200)");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_id", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general' CHECK(length(trim(lesson_group_id)) BETWEEN 1 AND 120)");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "lesson_group_title", "ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General' CHECK(length(trim(lesson_group_title)) BETWEEN 1 AND 200)");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "source_path", "ALTER TABLE practice_exercises ADD COLUMN source_path TEXT NOT NULL DEFAULT ''");
|
|
|
|
|
}
|
|
|
|
|
if (ensured) {
|
|
|
|
|
ensured = co_await ensure_column(connection, "practice_exercises", "catalog_managed", "ALTER TABLE practice_exercises ADD COLUMN catalog_managed INTEGER NOT NULL DEFAULT 1 CHECK(catalog_managed IN (0, 1))");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto index_statements = std::vector<std::string>{
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title COLLATE NOCASE)",
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_songs_title_part ON songs(title COLLATE NOCASE, part COLLATE NOCASE)",
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_songs_catalog_managed ON songs(catalog_managed)",
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_title ON practice_exercises(title COLLATE NOCASE)",
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id COLLATE NOCASE, lesson_group_id COLLATE NOCASE, title COLLATE NOCASE)",
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_practice_exercises_catalog_managed ON practice_exercises(catalog_managed)",
|
|
|
|
|
};
|
|
|
|
|
for (auto& statement : index_statements) {
|
|
|
|
|
if (!ensured) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
ensured = co_await execute_schema_statement(connection, std::move(statement));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ensured) {
|
|
|
|
|
auto committed = co_await connection.async_execute("COMMIT");
|
|
|
|
|
if (!committed) {
|
|
|
|
|
ensured = std::unexpected(committed.error());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
static_cast<void>(co_await connection.async_execute("ROLLBACK"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ensured) {
|
|
|
|
|
co_return std::unexpected(ensured.error());
|
|
|
|
|
}
|
|
|
|
|
auto version = co_await connection.async_set_user_version(0);
|
|
|
|
|
if (!version) {
|
|
|
|
|
co_return std::unexpected(version.error());
|
|
|
|
|
}
|
|
|
|
|
co_return std::expected<void, modulus::error>{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto path_parts(std::filesystem::path const& path) -> std::vector<std::string> {
|
|
|
|
|
auto parts = std::vector<std::string>{};
|
|
|
|
|
for (auto const& part : path) {
|
|
|
|
@@ -136,42 +247,9 @@ auto execute_delete_stale_catalog_rows(noto::connection& connection, std::string
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto vamp_zero::split_migration_statements(std::string const& input) -> std::expected<std::vector<std::string>, modulus::error> {
|
|
|
|
|
auto statements = std::vector<std::string>{};
|
|
|
|
|
auto current = std::string{};
|
|
|
|
|
auto stream = std::istringstream{input};
|
|
|
|
|
auto line = std::string{};
|
|
|
|
|
while (std::getline(stream, line)) {
|
|
|
|
|
if (trim(line) == statement_delimiter) {
|
|
|
|
|
current = trim(std::move(current));
|
|
|
|
|
if (current.empty()) {
|
|
|
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "migration contains an empty statement"));
|
|
|
|
|
}
|
|
|
|
|
statements.push_back(std::move(current));
|
|
|
|
|
current.clear();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
current += line;
|
|
|
|
|
current.push_back('\n');
|
|
|
|
|
}
|
|
|
|
|
current = trim(std::move(current));
|
|
|
|
|
if (!current.empty()) {
|
|
|
|
|
statements.push_back(std::move(current));
|
|
|
|
|
}
|
|
|
|
|
if (statements.empty()) {
|
|
|
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "migration must contain at least one statement"));
|
|
|
|
|
}
|
|
|
|
|
for (auto const& statement : statements) {
|
|
|
|
|
if (statement.empty() || statement.back() != ';') {
|
|
|
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "every migration statement must end with a semicolon"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
if (config.path.empty() || config.migrations_path.empty()) {
|
|
|
|
|
co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "database paths must not be empty"));
|
|
|
|
|
auto vamp_zero::async_ensure_database(boost::asio::any_io_executor executor, vamp_zero::database_config config) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
if (config.path.empty()) {
|
|
|
|
|
co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, "database path must not be empty"));
|
|
|
|
|
}
|
|
|
|
|
if (config.path != ":memory:") {
|
|
|
|
|
auto parent = std::filesystem::path{config.path}.parent_path();
|
|
|
|
@@ -213,76 +291,13 @@ auto vamp_zero::async_migrate_database(boost::asio::any_io_executor executor, va
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto foundation_statements = read_migration(config, "001_foundation.sql");
|
|
|
|
|
if (!foundation_statements) {
|
|
|
|
|
co_return std::unexpected(foundation_statements.error());
|
|
|
|
|
}
|
|
|
|
|
auto import_statements = read_migration(config, "002_song_import_metadata.sql");
|
|
|
|
|
if (!import_statements) {
|
|
|
|
|
co_return std::unexpected(import_statements.error());
|
|
|
|
|
}
|
|
|
|
|
auto practice_statements = read_migration(config, "003_practice_catalog.sql");
|
|
|
|
|
if (!practice_statements) {
|
|
|
|
|
co_return std::unexpected(practice_statements.error());
|
|
|
|
|
}
|
|
|
|
|
auto practice_hierarchy_statements = read_migration(config, "004_practice_hierarchy.sql");
|
|
|
|
|
if (!practice_hierarchy_statements) {
|
|
|
|
|
co_return std::unexpected(practice_hierarchy_statements.error());
|
|
|
|
|
}
|
|
|
|
|
auto catalog_managed_statements = read_migration(config, "005_catalog_managed_rows.sql");
|
|
|
|
|
if (!catalog_managed_statements) {
|
|
|
|
|
co_return std::unexpected(catalog_managed_statements.error());
|
|
|
|
|
}
|
|
|
|
|
auto migrator = noto::migrator{connection};
|
|
|
|
|
auto migrations = std::vector<noto::migration>{};
|
|
|
|
|
auto foundation = noto::migration{};
|
|
|
|
|
foundation.id = "001_foundation";
|
|
|
|
|
foundation.description = "initial Product Vamp appliance tables";
|
|
|
|
|
foundation.checksum = "001_foundation_v1";
|
|
|
|
|
foundation.up = [statements = std::move(*foundation_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
co_return co_await execute_statements(connection, statements);
|
|
|
|
|
};
|
|
|
|
|
migrations.push_back(std::move(foundation));
|
|
|
|
|
auto import = noto::migration{};
|
|
|
|
|
import.id = "002_song_import_metadata";
|
|
|
|
|
import.description = "song import metadata";
|
|
|
|
|
import.checksum = "002_song_import_metadata_v1";
|
|
|
|
|
import.up = [statements = std::move(*import_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
co_return co_await execute_statements(connection, statements);
|
|
|
|
|
};
|
|
|
|
|
migrations.push_back(std::move(import));
|
|
|
|
|
auto practice = noto::migration{};
|
|
|
|
|
practice.id = "003_practice_catalog";
|
|
|
|
|
practice.description = "practice exercise catalog";
|
|
|
|
|
practice.checksum = "003_practice_catalog_v1";
|
|
|
|
|
practice.up = [statements = std::move(*practice_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
co_return co_await execute_statements(connection, statements);
|
|
|
|
|
};
|
|
|
|
|
migrations.push_back(std::move(practice));
|
|
|
|
|
auto practice_hierarchy = noto::migration{};
|
|
|
|
|
practice_hierarchy.id = "004_practice_hierarchy";
|
|
|
|
|
practice_hierarchy.description = "practice type and lesson hierarchy";
|
|
|
|
|
practice_hierarchy.checksum = "004_practice_hierarchy_v1";
|
|
|
|
|
practice_hierarchy.up = [statements = std::move(*practice_hierarchy_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
co_return co_await execute_statements(connection, statements);
|
|
|
|
|
};
|
|
|
|
|
migrations.push_back(std::move(practice_hierarchy));
|
|
|
|
|
auto catalog_managed = noto::migration{};
|
|
|
|
|
catalog_managed.id = "005_catalog_managed_rows";
|
|
|
|
|
catalog_managed.description = "mark rows managed by bundled catalog import";
|
|
|
|
|
catalog_managed.checksum = "005_catalog_managed_rows_v1";
|
|
|
|
|
catalog_managed.up = [statements = std::move(*catalog_managed_statements)](noto::connection& connection) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
|
|
|
|
co_return co_await execute_statements(connection, statements);
|
|
|
|
|
};
|
|
|
|
|
migrations.push_back(std::move(catalog_managed));
|
|
|
|
|
|
|
|
|
|
auto migrated = co_await migrator.async_migrate(std::move(migrations));
|
|
|
|
|
if (!migrated) {
|
|
|
|
|
co_return std::unexpected(migrated.error());
|
|
|
|
|
}
|
|
|
|
|
auto version = co_await connection.async_set_user_version(5);
|
|
|
|
|
if (!version) {
|
|
|
|
|
co_return std::unexpected(version.error());
|
|
|
|
|
auto ensured = co_await ensure_current_schema(connection);
|
|
|
|
|
if (!ensured) {
|
|
|
|
|
auto close = co_await connection.async_close();
|
|
|
|
|
if (!close) {
|
|
|
|
|
modulus::log_error("database close failed after schema ensure error: " + close.error().describe());
|
|
|
|
|
}
|
|
|
|
|
co_return std::unexpected(ensured.error());
|
|
|
|
|
}
|
|
|
|
|
co_return co_await connection.async_close();
|
|
|
|
|
}
|
|
|
|
|