+11
-11
@@ -41,7 +41,7 @@ auto invalid(std::string message) -> modulus::error {
|
||||
}
|
||||
|
||||
auto usage() -> std::string {
|
||||
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--migrate]";
|
||||
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--ensure-database]";
|
||||
}
|
||||
|
||||
auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
@@ -88,9 +88,9 @@ auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, mod
|
||||
return write_text_atomic(config.runtime.heartbeat_path, boost::json::serialize(heartbeat));
|
||||
}
|
||||
|
||||
auto migrate(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto ensure_database(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_migrate_database(io.get_executor(), config.database), boost::asio::use_future);
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), config.database), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
@@ -258,8 +258,8 @@ auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std
|
||||
result.print_version = true;
|
||||
} else if (argument == "--once") {
|
||||
result.exit_after_startup = true;
|
||||
} else if (argument == "--migrate") {
|
||||
result.migrate = true;
|
||||
} else if (argument == "--ensure-database") {
|
||||
result.ensure_database = true;
|
||||
} else if (argument == "--help" || argument == "-h") {
|
||||
return std::unexpected(invalid(usage()));
|
||||
} else {
|
||||
@@ -270,7 +270,7 @@ auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std
|
||||
+ static_cast<std::uint32_t>(result.health_check ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.print_version ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.migrate ? 1 : 0);
|
||||
+ static_cast<std::uint32_t>(result.ensure_database ? 1 : 0);
|
||||
if (command_count > 1U) {
|
||||
return std::unexpected(invalid("only one command flag may be selected"));
|
||||
}
|
||||
@@ -295,17 +295,17 @@ auto vamp_zero::run(vamp_zero::command_line command) -> std::expected<void, modu
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
if (command.migrate) {
|
||||
return migrate(*config);
|
||||
if (command.ensure_database) {
|
||||
return ensure_database(*config);
|
||||
}
|
||||
if (command.exit_after_startup || config->runtime.exit_after_startup) {
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto migrated = migrate(*config);
|
||||
if (!migrated) {
|
||||
return migrated;
|
||||
auto ensured = ensure_database(*config);
|
||||
if (!ensured) {
|
||||
return ensured;
|
||||
}
|
||||
auto imported = import_library(*config);
|
||||
if (!imported) {
|
||||
|
||||
@@ -116,13 +116,11 @@ auto parse_database(boost::json::object const& root, vamp_zero::database_config
|
||||
return fallback;
|
||||
}
|
||||
auto path = optional_string(**object, "path", fallback.path);
|
||||
auto migrations_path = optional_string(**object, "migrations_path", fallback.migrations_path);
|
||||
if (!path || !migrations_path) {
|
||||
return std::unexpected(!path ? path.error() : migrations_path.error());
|
||||
if (!path) {
|
||||
return std::unexpected(path.error());
|
||||
}
|
||||
return vamp_zero::database_config{
|
||||
.path = std::move(*path),
|
||||
.migrations_path = std::move(*migrations_path),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+164
-149
@@ -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();
|
||||
}
|
||||
|
||||
@@ -554,7 +554,6 @@ auto vamp_zero::remote_display_text(vamp_zero::app_state const& state) -> std::s
|
||||
}
|
||||
|
||||
auto output = std::ostringstream{};
|
||||
output << "version=1\n";
|
||||
output << "mode=" << vamp_zero::to_string(state.mode) << "\n";
|
||||
output << "active=" << (active ? "1" : "0") << "\n";
|
||||
output << "title=" << sanitize_line_value(title) << "\n";
|
||||
|
||||
Reference in New Issue
Block a user